mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: initial enforcement of template policy
This commit is contained in:
24
backend/package-lock.json
generated
24
backend/package-lock.json
generated
@@ -75,6 +75,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -106,6 +107,7 @@
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@@ -9774,6 +9776,12 @@
|
||||
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/safe-regex": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz",
|
||||
"integrity": "sha512-CQ/uPB9fLOPKwDsrTeVbNIkwfUthTWOx0l6uIGwVFjZxv7e68pCW5gtTYFzdJi3EBJp8h8zYhJbTasAbX7gEMQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
@@ -17778,6 +17786,14 @@
|
||||
"@babel/runtime": "^7.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp-tree": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||
"bin": {
|
||||
"regexp-tree": "bin/regexp-tree"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||
@@ -18094,6 +18110,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safe-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
|
||||
"dependencies": {
|
||||
"regexp-tree": "~0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@@ -171,6 +172,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
|
||||
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -53,6 +53,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate,
|
||||
TDynamicSecretLeases,
|
||||
TDynamicSecretLeasesInsert,
|
||||
TDynamicSecretLeasesUpdate,
|
||||
@@ -318,11 +321,6 @@ import {
|
||||
TWebhooksInsert,
|
||||
TWebhooksUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate
|
||||
} from "@app/db/schemas/certificate-templates";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
TSecretV2TagJunctionInsert,
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
|
||||
@@ -1088,6 +1088,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
},
|
||||
ISSUE_CERT: {
|
||||
caId: "The ID of the CA to issue the certificate from",
|
||||
certificateTemplateId: "The ID of the certificate template to issue the certificate from",
|
||||
friendlyName: "A friendly name for the certificate",
|
||||
commonName: "The common name (CN) for the certificate",
|
||||
altNames:
|
||||
|
||||
@@ -622,6 +622,7 @@ export const registerRoutes = async (
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
|
||||
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -55,6 +60,183 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/issue-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Issue certificate",
|
||||
body: z
|
||||
.object({
|
||||
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
|
||||
certificateTemplateId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
|
||||
(data.caId === undefined && data.certificateTemplateId !== undefined),
|
||||
{
|
||||
message: "Either CA ID or Certificate Template ID must be present, but not both",
|
||||
path: ["caId", "certificateTemplateId"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.issueCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/sign-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Sign certificate",
|
||||
body: z
|
||||
.object({
|
||||
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId),
|
||||
certificateTemplateId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
|
||||
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
|
||||
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
|
||||
(data.caId === undefined && data.certificateTemplateId !== undefined),
|
||||
{
|
||||
message: "Either CA ID or Certificate Template ID must be present, but not both",
|
||||
path: ["caId", "certificateTemplateId"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:serialNumber/revoke",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateTemplatesSchema } from "@app/db/schemas/certificate-templates";
|
||||
import { CertificateTemplatesSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
||||
id: true,
|
||||
@@ -58,9 +60,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
schema: {
|
||||
body: z.object({
|
||||
caId: z.string(),
|
||||
name: z.string(),
|
||||
commonName: z.string(),
|
||||
ttl: z.string()
|
||||
name: z.string().min(1),
|
||||
commonName: validateTemplateRegexField,
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -91,9 +93,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
schema: {
|
||||
body: z.object({
|
||||
caId: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
commonName: z.string().optional(),
|
||||
ttl: z.string().optional()
|
||||
name: z.string().min(1).optional(),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
}),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string()
|
||||
|
||||
@@ -4,11 +4,11 @@ import { z } from "zod";
|
||||
import {
|
||||
CertificateAuthoritiesSchema,
|
||||
CertificatesSchema,
|
||||
CertificateTemplatesSchema,
|
||||
PkiAlertsSchema,
|
||||
PkiCollectionsSchema,
|
||||
ProjectKeysSchema
|
||||
} from "@app/db/schemas";
|
||||
import { CertificateTemplatesSchema } from "@app/db/schemas/certificate-templates";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -478,7 +478,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
name: true
|
||||
}).merge(
|
||||
z.object({
|
||||
caName: z.string()
|
||||
caName: z.string(),
|
||||
caId: z.string()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,9 +5,10 @@ import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@@ -16,6 +17,8 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import {
|
||||
@@ -54,6 +57,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
@@ -69,6 +73,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
projectDAL,
|
||||
@@ -1001,6 +1006,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
*/
|
||||
const issueCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
@@ -1012,8 +1018,25 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
if (caId) {
|
||||
ca = await certificateAuthorityDAL.findById(caId);
|
||||
} else if (certificateTemplateId) {
|
||||
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
|
||||
}
|
||||
|
||||
if (!ca) {
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -1073,6 +1096,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName,
|
||||
notBeforeDate,
|
||||
notAfterDate
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
@@ -1207,6 +1241,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
*/
|
||||
const signCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
friendlyName,
|
||||
commonName,
|
||||
@@ -1219,8 +1254,25 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
if (caId) {
|
||||
ca = await certificateAuthorityDAL.findById(caId);
|
||||
} else if (certificateTemplateId) {
|
||||
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
|
||||
}
|
||||
|
||||
if (!ca) {
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -1293,6 +1345,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||
});
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName: cn,
|
||||
notBeforeDate,
|
||||
notAfterDate
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
|
||||
@@ -86,7 +86,8 @@ export type TImportCertToCaDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
caId: string;
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
@@ -96,8 +97,9 @@ export type TIssueCertFromCaDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO = {
|
||||
caId: string;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import ms from "ms";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
export const validateCertificateDetailsAgainstTemplate = (
|
||||
cert: {
|
||||
commonName: string;
|
||||
notBeforeDate: Date;
|
||||
notAfterDate: Date;
|
||||
},
|
||||
template: {
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
}
|
||||
) => {
|
||||
const commonNameRegex = new RegExp(template.commonName);
|
||||
if (!commonNameRegex.test(cert.commonName)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid common name based on template policy"
|
||||
});
|
||||
}
|
||||
|
||||
if (cert.notAfterDate.getTime() - cert.notBeforeDate.getTime() > ms(template.ttl)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid validity date based on template policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import safe from "safe-regex";
|
||||
import z from "zod";
|
||||
|
||||
export const validateTemplateRegexField = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
})
|
||||
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||
.refine((v) => safe(v), {
|
||||
message: "Unsafe REGEX pattern"
|
||||
});
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
TRenewCaResponse,
|
||||
TSignIntermediateDTO,
|
||||
TSignIntermediateResponse,
|
||||
TUpdateCaDTO} from "./types";
|
||||
TUpdateCaDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -98,9 +99,9 @@ export const useImportCaCertificate = () => {
|
||||
export const useCreateCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||
`/api/v1/pki/ca/${caId}/issue-certificate`,
|
||||
"/api/v1/pki/certificates/issue-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||
import { CaRenewalType,CaStatus, CaType } from "./enums";
|
||||
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
id: string;
|
||||
@@ -78,7 +78,8 @@ export type TImportCaCertificateResponse = {
|
||||
|
||||
export type TCreateCertificateDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string; // sans
|
||||
|
||||
@@ -2,6 +2,7 @@ export type TCertificateTemplateListEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
caName: string;
|
||||
caId: string;
|
||||
};
|
||||
|
||||
export type TCertificateTemplate = {
|
||||
|
||||
@@ -14,13 +14,21 @@ import {
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreateCertificate,
|
||||
useGetCert,
|
||||
useGetCertTemplate,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificateTemplates
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
const schema = z.object({
|
||||
certificateTemplateId: z.string().optional(),
|
||||
caId: z.string(),
|
||||
friendlyName: z.string(),
|
||||
commonName: z.string().trim().min(1),
|
||||
@@ -42,6 +50,8 @@ type TCertificateDetails = {
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
const CERT_TEMPLATE_NONE_VALUE = "none";
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -54,6 +64,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||
|
||||
const {
|
||||
@@ -61,11 +75,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const selectedCertTemplateId = watch("certificateTemplateId");
|
||||
const hasCertTemplateSelected =
|
||||
selectedCertTemplateId !== "" && selectedCertTemplateId !== CERT_TEMPLATE_NONE_VALUE;
|
||||
|
||||
const { data: selectedCertTemplate } = useGetCertTemplate(
|
||||
hasCertTemplateSelected ? (selectedCertTemplateId as string) : ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
reset({
|
||||
@@ -81,7 +104,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
friendlyName: "",
|
||||
commonName: "",
|
||||
altNames: "",
|
||||
ttl: ""
|
||||
ttl: "",
|
||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE
|
||||
});
|
||||
}
|
||||
}, [cert]);
|
||||
@@ -91,8 +115,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||
caId: !selectedCertTemplate ? caId : undefined,
|
||||
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
@@ -141,11 +166,11 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
name="certificateTemplateId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
label="Certificate Template"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
@@ -158,15 +183,47 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
|
||||
None
|
||||
</SelectItem>
|
||||
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`cert-template-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{(!selectedCertTemplateId || selectedCertTemplateId === CERT_TEMPLATE_NONE_VALUE) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
@@ -27,7 +27,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
const schema = z.object({
|
||||
caId: z.string(),
|
||||
name: z.string().min(1),
|
||||
commonName: z.string().trim().min(1),
|
||||
commonName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message:
|
||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
}),
|
||||
ttl: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user