Skip to content

Commit 11d80cd

Browse files
Merge pull request #7059 from Infisical/feat/PKI-152
feat: add DigiCert code-sign support
2 parents 39dea09 + 943b69a commit 11d80cd

44 files changed

Lines changed: 3068 additions & 933 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Knex } from "knex";
2+
3+
import { TableName } from "../schemas";
4+
5+
const INDEX_NAME = "certificates_digicert_order_idx";
6+
const MIGRATION_TIMEOUT = 60 * 60 * 1000;
7+
const MIGRATION_LOCK_TIMEOUT = 30 * 1000;
8+
9+
export async function up(knex: Knex): Promise<void> {
10+
const stmtResult = await knex.raw("SHOW statement_timeout");
11+
const originalStatementTimeout = stmtResult.rows[0].statement_timeout;
12+
const lockResult = await knex.raw("SHOW lock_timeout");
13+
const originalLockTimeout = lockResult.rows[0].lock_timeout;
14+
15+
try {
16+
await knex.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
17+
await knex.raw(`SET lock_timeout = ${MIGRATION_LOCK_TIMEOUT}`);
18+
19+
if (
20+
(await knex.schema.hasTable(TableName.Certificate)) &&
21+
(await knex.schema.hasColumn(TableName.Certificate, "externalMetadata"))
22+
) {
23+
await knex.raw(`
24+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "${INDEX_NAME}"
25+
ON ${TableName.Certificate} ("caId", ("externalMetadata"->>'orderId'))
26+
WHERE "externalMetadata"->>'type' = 'digicert'
27+
`);
28+
}
29+
} finally {
30+
await knex.raw(`SET statement_timeout = '${originalStatementTimeout}'`);
31+
await knex.raw(`SET lock_timeout = '${originalLockTimeout}'`);
32+
}
33+
}
34+
35+
export async function down(knex: Knex): Promise<void> {
36+
await knex.raw(`DROP INDEX CONCURRENTLY IF EXISTS "${INDEX_NAME}"`);
37+
}
38+
39+
const config = { transaction: false };
40+
export { config };

backend/src/ee/services/audit-log/audit-log-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
} from "@app/services/secret-sync/secret-sync-types";
5656
import { TDuplicateSecretAttributes } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
5757
import { CertKeySource } from "@app/services/signer/signer-enums";
58+
import { TSignerExternalConfigurationInput } from "@app/services/signer/signer-types";
5859
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
5960
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
6061

@@ -4929,6 +4930,7 @@ interface CreatePkiSignerEvent {
49294930
approvalPolicyId?: string | null;
49304931
keySource?: CertKeySource;
49314932
hsmConnectorId?: string | null;
4933+
externalConfiguration?: TSignerExternalConfigurationInput;
49324934
};
49334935
}
49344936

@@ -5017,6 +5019,7 @@ interface ReissuePkiSignerCertificateEvent {
50175019
keySource?: string;
50185020
hsmConnectorId?: string;
50195021
hsmKeyAlgorithm?: string;
5022+
externalConfiguration?: TSignerExternalConfigurationInput;
50205023
};
50215024
}
50225025

backend/src/server/routes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,6 +3321,7 @@ export const registerRoutes = async (
33213321
certificateAuthorityDAL,
33223322
signerIssuanceService,
33233323
internalCertificateAuthorityService,
3324+
digicertFns: digicertCaFns,
33243325
projectDAL,
33253326
kmsService,
33263327
permissionService,

backend/src/server/routes/v1/app-connection-routers/digicert-connection-router.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,78 @@ export const registerDigiCertConnectionRouter = async (server: FastifyZodProvide
7878
return server.services.appConnection.digicert.listProducts(connectionId, req.permission);
7979
}
8080
});
81+
82+
server.route({
83+
method: "GET",
84+
url: `/:connectionId/organizations/:organizationId/validation`,
85+
config: {
86+
rateLimit: readLimit
87+
},
88+
schema: {
89+
operationId: "getDigiCertOrgValidation",
90+
params: z.object({
91+
connectionId: z.string().uuid(),
92+
organizationId: z.coerce.number().int().positive()
93+
}),
94+
querystring: z.object({
95+
productNameId: z.string().trim().min(1)
96+
}),
97+
response: {
98+
200: z.object({
99+
isValidated: z.boolean()
100+
})
101+
}
102+
},
103+
onRequest: verifyAuth([AuthMode.JWT]),
104+
handler: async (req) => {
105+
const { connectionId, organizationId } = req.params;
106+
const { productNameId } = req.query;
107+
return server.services.appConnection.digicert.getOrgValidation(
108+
connectionId,
109+
organizationId,
110+
productNameId,
111+
req.permission
112+
);
113+
}
114+
});
115+
116+
server.route({
117+
method: "GET",
118+
url: `/:connectionId/organizations/:organizationId/orders`,
119+
config: {
120+
rateLimit: readLimit
121+
},
122+
schema: {
123+
operationId: "listDigiCertOrders",
124+
params: z.object({
125+
connectionId: z.string().uuid(),
126+
organizationId: z.coerce.number().int().positive()
127+
}),
128+
querystring: z.object({
129+
productNameId: z.string().trim().min(1)
130+
}),
131+
response: {
132+
200: z
133+
.object({
134+
orderId: z.number(),
135+
commonName: z.string(),
136+
organizationName: z.string(),
137+
status: z.string(),
138+
validTill: z.string().optional()
139+
})
140+
.array()
141+
}
142+
},
143+
onRequest: verifyAuth([AuthMode.JWT]),
144+
handler: async (req) => {
145+
const { connectionId, organizationId } = req.params;
146+
const { productNameId } = req.query;
147+
return server.services.appConnection.digicert.listOrders(
148+
connectionId,
149+
organizationId,
150+
productNameId,
151+
req.permission
152+
);
153+
}
154+
});
81155
};

backend/src/server/routes/v1/signer-routers/certificate-router.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
88
import { AuthMode } from "@app/services/auth/auth-type";
99
import { CertKeySource } from "@app/services/signer/signer-enums";
1010

11-
import { HSM_SUPPORTED_KEY_ALGORITHMS, SignerIdParamsSchema, SignerKeyAlgorithm } from "./schemas";
11+
import {
12+
HSM_SUPPORTED_KEY_ALGORITHMS,
13+
SignerExternalConfigurationSchema,
14+
SignerIdParamsSchema,
15+
SignerKeyAlgorithm
16+
} from "./schemas";
1217

1318
export const registerSignerCertificateRouter = async (server: FastifyZodProvider) => {
1419
server.route({
@@ -32,7 +37,8 @@ export const registerSignerCertificateRouter = async (server: FastifyZodProvider
3237
keySource: z.nativeEnum(CertKeySource),
3338
hsmConnectorId: z.string().uuid().optional()
3439
})
35-
.optional()
40+
.optional(),
41+
externalConfiguration: SignerExternalConfigurationSchema.optional()
3642
})
3743
.superRefine((data, ctx) => {
3844
if (data.certificate?.keySource !== CertKeySource.Hsm) return;
@@ -76,7 +82,8 @@ export const registerSignerCertificateRouter = async (server: FastifyZodProvider
7682
commonName: req.body.commonName,
7783
keyAlgorithm: req.body.keyAlgorithm,
7884
keySource: req.body.certificate?.keySource,
79-
hsmConnectorId: req.body.certificate?.hsmConnectorId
85+
hsmConnectorId: req.body.certificate?.hsmConnectorId,
86+
externalConfiguration: req.body.externalConfiguration
8087
}
8188
}
8289
});

backend/src/server/routes/v1/signer-routers/lifecycle-router.ts

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,32 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
1414
import {
1515
ApprovalPolicyBodySchema,
1616
HSM_SUPPORTED_KEY_ALGORITHMS,
17+
SignerExternalConfigurationSchema,
1718
SignerIdParamsSchema,
1819
SignerKeyAlgorithm
1920
} from "./schemas";
2021

22+
const SignerWithCertificateResponseSchema = PkiSignersSchema.extend({
23+
certificateCommonName: z.string().nullable().optional(),
24+
certificateSerialNumber: z.string().nullable().optional(),
25+
certificateNotAfter: z.date().nullable().optional(),
26+
certificateNotBefore: z.date().nullable().optional(),
27+
certificateKeyAlgorithm: z.string().nullable().optional(),
28+
certificateKeySource: z.string().nullable().optional(),
29+
certificateHsmConnectorId: z.string().nullable().optional(),
30+
certificateStatus: z.string().nullable().optional(),
31+
certificateCaId: z.string().nullable().optional(),
32+
approvalPolicyName: z.string().nullable().optional(),
33+
externalOrder: z
34+
.object({
35+
provider: z.string(),
36+
orderId: z.number(),
37+
status: z.string().nullable()
38+
})
39+
.nullable()
40+
.optional()
41+
});
42+
2143
export const registerSignerLifecycleRouter = async (server: FastifyZodProvider) => {
2244
server.route({
2345
method: "POST",
@@ -45,6 +67,7 @@ export const registerSignerLifecycleRouter = async (server: FastifyZodProvider)
4567
hsmConnectorId: z.string().uuid().optional()
4668
})
4769
.optional(),
70+
externalConfiguration: SignerExternalConfigurationSchema.optional(),
4871
approvalPolicyId: z.string().uuid().optional(),
4972
members: z
5073
.array(
@@ -100,7 +123,8 @@ export const registerSignerLifecycleRouter = async (server: FastifyZodProvider)
100123
certificateId: signer.certificateId,
101124
approvalPolicyId: signer.approvalPolicyId,
102125
keySource: req.body.certificate?.keySource,
103-
hsmConnectorId: req.body.certificate?.hsmConnectorId
126+
hsmConnectorId: req.body.certificate?.hsmConnectorId,
127+
externalConfiguration: req.body.externalConfiguration
104128
}
105129
}
106130
});
@@ -187,18 +211,7 @@ export const registerSignerLifecycleRouter = async (server: FastifyZodProvider)
187211
description: "Get a code signing signer by ID",
188212
params: SignerIdParamsSchema,
189213
response: {
190-
200: PkiSignersSchema.extend({
191-
certificateCommonName: z.string().nullable().optional(),
192-
certificateSerialNumber: z.string().nullable().optional(),
193-
certificateNotAfter: z.date().nullable().optional(),
194-
certificateNotBefore: z.date().nullable().optional(),
195-
certificateKeyAlgorithm: z.string().nullable().optional(),
196-
certificateKeySource: z.string().nullable().optional(),
197-
certificateHsmConnectorId: z.string().nullable().optional(),
198-
certificateStatus: z.string().nullable().optional(),
199-
certificateCaId: z.string().nullable().optional(),
200-
approvalPolicyName: z.string().nullable().optional()
201-
})
214+
200: SignerWithCertificateResponseSchema
202215
}
203216
},
204217
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -267,6 +280,34 @@ export const registerSignerLifecycleRouter = async (server: FastifyZodProvider)
267280
}
268281
});
269282

283+
server.route({
284+
method: "POST",
285+
url: "/:signerId/issuance/check",
286+
config: { rateLimit: writeLimit },
287+
schema: {
288+
hide: false,
289+
operationId: "checkSignerIssuance",
290+
tags: [ApiDocsTags.PkiSigners],
291+
description:
292+
"Poll the upstream CA for a pending signer's certificate immediately instead of waiting for the next scheduled check.",
293+
params: SignerIdParamsSchema,
294+
response: {
295+
200: SignerWithCertificateResponseSchema
296+
}
297+
},
298+
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
299+
handler: async (req) => {
300+
const signer = await server.services.pkiSigner.checkIssuanceNow({
301+
signerId: req.params.signerId,
302+
actor: req.permission.type,
303+
actorId: req.permission.id,
304+
actorAuthMethod: req.permission.authMethod,
305+
actorOrgId: req.permission.orgId
306+
});
307+
return signer;
308+
}
309+
});
310+
270311
server.route({
271312
method: "PATCH",
272313
url: "/:signerId",

backend/src/server/routes/v1/signer-routers/schemas.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import { z } from "zod";
22

33
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
4+
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
45

56
export const SignerIdParamsSchema = z.object({ signerId: z.string().uuid() });
67

8+
export const SignerExternalConfigurationSchema = z.discriminatedUnion("caType", [
9+
z.object({
10+
caType: z.literal(CaType.DIGICERT),
11+
reissueFromExternalOrderId: z
12+
.string()
13+
.trim()
14+
.min(1)
15+
.optional()
16+
.describe(
17+
"Reissue into this existing DigiCert order instead of placing a new order (reuses the subscription slot)."
18+
)
19+
})
20+
]);
21+
722
export const SignerKeyAlgorithm = {
823
values: [
924
CertKeyAlgorithm.RSA_2048,

backend/src/services/app-connection/digicert/digicert-connection-errors.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ type TDigiCertErrorResponse = { errors?: { code?: string; message?: string }[] }
55
export const extractDigiCertErrorMessage = (error: unknown) => {
66
if (error instanceof AxiosError) {
77
const data = error.response?.data as TDigiCertErrorResponse | undefined;
8-
const firstError = data?.errors?.[0];
9-
if (firstError?.message) {
10-
return firstError.code ? `${firstError.message} (${firstError.code})` : firstError.message;
8+
// Include every error code, not just the first, so callers can match a code in any position.
9+
const errors = (data?.errors ?? []).filter((e) => e?.message || e?.code);
10+
if (errors.length) {
11+
return errors.map((e) => (e.code ? `${e.message ?? ""} (${e.code})` : e.message)).join("; ");
1112
}
1213
return error.message || "Unknown error";
1314
}

0 commit comments

Comments
 (0)