mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 23:48:05 -05:00
Add sign certificate endpoint
This commit is contained in:
@@ -136,6 +136,7 @@ export enum EventType {
|
|||||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||||
GET_CA_CRL = "get-certificate-authority-crl",
|
GET_CA_CRL = "get-certificate-authority-crl",
|
||||||
ISSUE_CERT = "issue-cert",
|
ISSUE_CERT = "issue-cert",
|
||||||
|
SIGN_CERT = "sign-cert",
|
||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
DELETE_CERT = "delete-cert",
|
DELETE_CERT = "delete-cert",
|
||||||
REVOKE_CERT = "revoke-cert",
|
REVOKE_CERT = "revoke-cert",
|
||||||
@@ -1143,6 +1144,15 @@ interface IssueCert {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignCert {
|
||||||
|
type: EventType.SIGN_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetCert {
|
interface GetCert {
|
||||||
type: EventType.GET_CERT;
|
type: EventType.GET_CERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1333,6 +1343,7 @@ export type Event =
|
|||||||
| ImportCaCert
|
| ImportCaCert
|
||||||
| GetCaCrl
|
| GetCaCrl
|
||||||
| IssueCert
|
| IssueCert
|
||||||
|
| SignCert
|
||||||
| GetCert
|
| GetCert
|
||||||
| DeleteCert
|
| DeleteCert
|
||||||
| RevokeCert
|
| RevokeCert
|
||||||
|
|||||||
@@ -1056,7 +1056,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
},
|
},
|
||||||
SIGN_INTERMEDIATE: {
|
SIGN_INTERMEDIATE: {
|
||||||
caId: "The ID of the CA to sign the intermediate certificate with",
|
caId: "The ID of the CA to sign the intermediate certificate with",
|
||||||
csr: "The CSR to sign with the CA",
|
csr: "The pem-encoded CSR to sign with the CA",
|
||||||
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
maxPathLength:
|
maxPathLength:
|
||||||
@@ -1086,6 +1086,21 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
privateKey: "The private key of the issued certificate",
|
privateKey: "The private key of the issued certificate",
|
||||||
serialNumber: "The serial number of the issued certificate"
|
serialNumber: "The serial number of the issued certificate"
|
||||||
},
|
},
|
||||||
|
SIGN_CERT: {
|
||||||
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
|
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
|
||||||
|
friendlyName: "A friendly name for the certificate",
|
||||||
|
commonName: "The common name (CN) for the certificate",
|
||||||
|
altNames:
|
||||||
|
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
certificate: "The issued certificate",
|
||||||
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
|
certificateChain: "The certificate chain of the issued certificate",
|
||||||
|
serialNumber: "The serial number of the issued certificate"
|
||||||
|
},
|
||||||
GET_CRL: {
|
GET_CRL: {
|
||||||
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
||||||
crl: "The certificate revocation list (CRL) of the CA"
|
crl: "The certificate revocation list (CRL) of the CA"
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
||||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
||||||
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
||||||
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
||||||
@@ -453,7 +453,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||||
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||||
ttl: z
|
ttl: z
|
||||||
@@ -516,4 +516,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/sign-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Sign certificate from CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
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({
|
||||||
|
caId: req.params.caId,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,43 @@ export const createDistinguishedName = (parts: TDNParts) => {
|
|||||||
return dnParts.join(", ");
|
return dnParts.join(", ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseDistinguishedName = (dn: string): TDNParts => {
|
||||||
|
const parts: TDNParts = {};
|
||||||
|
const dnRegex = /(?:^|,\s*)([A-Z]+)=([^,]+)/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
match = dnRegex.exec(dn);
|
||||||
|
if (match === null) break;
|
||||||
|
|
||||||
|
const [, key, value] = match;
|
||||||
|
switch (key) {
|
||||||
|
case "C":
|
||||||
|
parts.country = value;
|
||||||
|
break;
|
||||||
|
case "O":
|
||||||
|
parts.organization = value;
|
||||||
|
break;
|
||||||
|
case "OU":
|
||||||
|
parts.ou = value;
|
||||||
|
break;
|
||||||
|
case "ST":
|
||||||
|
parts.province = value;
|
||||||
|
break;
|
||||||
|
case "CN":
|
||||||
|
parts.commonName = value;
|
||||||
|
break;
|
||||||
|
case "L":
|
||||||
|
parts.locality = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Ignore unrecognized keys
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
|
||||||
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
||||||
switch (keyAlgorithm) {
|
switch (keyAlgorithm) {
|
||||||
case CertKeyAlgorithm.RSA_4096:
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
createDistinguishedName,
|
createDistinguishedName,
|
||||||
getCaCertChain,
|
getCaCertChain,
|
||||||
getCaCredentials,
|
getCaCredentials,
|
||||||
keyAlgorithmToAlgCfg
|
keyAlgorithmToAlgCfg,
|
||||||
|
parseDistinguishedName
|
||||||
} from "./certificate-authority-fns";
|
} from "./certificate-authority-fns";
|
||||||
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
TGetCaDTO,
|
TGetCaDTO,
|
||||||
TImportCertToCaDTO,
|
TImportCertToCaDTO,
|
||||||
TIssueCertFromCaDTO,
|
TIssueCertFromCaDTO,
|
||||||
|
TSignCertFromCaDTO,
|
||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TUpdateCaDTO
|
TUpdateCaDTO
|
||||||
} from "./certificate-authority-types";
|
} from "./certificate-authority-types";
|
||||||
@@ -651,7 +653,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new leaf certificate issued by CA with id [caId]
|
* Return new leaf certificate issued by CA with id [caId] and private key.
|
||||||
|
* Note: private key and CSR are generated within Infisical.
|
||||||
*/
|
*/
|
||||||
const issueCertFromCa = async ({
|
const issueCertFromCa = async ({
|
||||||
caId,
|
caId,
|
||||||
@@ -851,6 +854,204 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new leaf certificate issued by CA with id [caId].
|
||||||
|
* Note: CSR is generated externally and submitted to Infisical.
|
||||||
|
*/
|
||||||
|
const signCertFromCa = async ({
|
||||||
|
caId,
|
||||||
|
csr,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
ttl,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TSignCertFromCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsDecryptor({
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
|
||||||
|
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
|
||||||
|
if (notAfter) {
|
||||||
|
notAfterDate = new Date(notAfter);
|
||||||
|
} else if (ttl) {
|
||||||
|
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
const dn = parseDistinguishedName(csrObj.subject);
|
||||||
|
const cn = commonName || dn.commonName;
|
||||||
|
|
||||||
|
if (!cn)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (altNames) {
|
||||||
|
const altNamesArray: {
|
||||||
|
type: "email" | "dns";
|
||||||
|
value: string;
|
||||||
|
}[] = altNames
|
||||||
|
.split(",")
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.map((altName) => {
|
||||||
|
// check if the altName is a valid email
|
||||||
|
if (z.string().email().safeParse(altName).success) {
|
||||||
|
return {
|
||||||
|
type: "email",
|
||||||
|
value: altName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the altName is a valid hostname
|
||||||
|
if (hostnameRegex.test(altName)) {
|
||||||
|
return {
|
||||||
|
type: "dns",
|
||||||
|
value: altName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
||||||
|
throw new Error(`Invalid altName: ${altName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
extensions.push(altNamesExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: csrObj.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: friendlyName || csrObj.subject,
|
||||||
|
commonName: cn,
|
||||||
|
altNames,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createCa,
|
createCa,
|
||||||
getCaById,
|
getCaById,
|
||||||
@@ -860,6 +1061,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
getCaCert,
|
getCaCert,
|
||||||
signIntermediate,
|
signIntermediate,
|
||||||
importCertToCa,
|
importCertToCa,
|
||||||
issueCertFromCa
|
issueCertFromCa,
|
||||||
|
signCertFromCa
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ export type TIssueCertFromCaDTO = {
|
|||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSignCertFromCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
csr: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName?: string;
|
||||||
|
altNames: string;
|
||||||
|
ttl: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDNParts = {
|
export type TDNParts = {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sign certificate"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/sign-certificate"
|
||||||
|
---
|
||||||
@@ -74,7 +74,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="API">
|
<Tab title="API">
|
||||||
To create a certificate, make an API request to the [Create Certificate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint,
|
To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
||||||
specifying the issuing CA.
|
specifying the issuing CA.
|
||||||
|
|
||||||
### Sample request
|
### Sample request
|
||||||
@@ -84,6 +84,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"commonName": "My Certificate",
|
"commonName": "My Certificate",
|
||||||
|
"ttl": "1y",
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,6 +104,31 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/issue-certificate' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"csr": "...",
|
||||||
|
"ttl": "1y",
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
certificate: "...",
|
||||||
|
certificateChain: "...",
|
||||||
|
issuingCaCertificate: "...",
|
||||||
|
privateKey: "...",
|
||||||
|
serialNumber: "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -671,6 +671,7 @@
|
|||||||
"api-reference/endpoints/certificate-authorities/sign-intermediate",
|
"api-reference/endpoints/certificate-authorities/sign-intermediate",
|
||||||
"api-reference/endpoints/certificate-authorities/import-cert",
|
"api-reference/endpoints/certificate-authorities/import-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/issue-cert",
|
"api-reference/endpoints/certificate-authorities/issue-cert",
|
||||||
|
"api-reference/endpoints/certificate-authorities/sign-cert",
|
||||||
"api-reference/endpoints/certificate-authorities/crl"
|
"api-reference/endpoints/certificate-authorities/crl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user