Allow empty CN

This commit is contained in:
Fang-Pen Lin
2025-11-21 10:30:55 -08:00
parent 02c346c77a
commit cf83ae2553
7 changed files with 23 additions and 18 deletions
@@ -1,13 +1,13 @@
Feature: Internal CA Feature: Internal CA
Scenario Outline: CSR with SANs only Scenario: CSR with SANs only
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
""" """
<names> {}
""" """
And I add subject alternative name to certificate signing request csr And I add subject alternative name to certificate signing request csr
""" """
@@ -25,8 +25,3 @@ Feature: Internal CA
And the value finalized_order.body with jq ".status" should be equal to "valid" And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost" And the value cert with jq ".subject.common_name" should be equal to "localhost"
Examples:
| names |
| {} |
| {"COMMON_NAME": ""}
@@ -75,7 +75,7 @@ export const pkiAcmeChallengeServiceFactory = ({
const challengeResponse = await axios.get<string>(challengeUrl.toString(), { const challengeResponse = await axios.get<string>(challengeUrl.toString(), {
// In case if we override the host in the development mode, still provide the original host in the header // In case if we override the host in the development mode, still provide the original host in the header
// to help the upstream server to validate the request // to help the upstream server to validate the request
headers: { Host: host }, headers: { Host: challenge.auth.identifierValue },
timeout: timeoutMs, timeout: timeoutMs,
responseType: "text", responseType: "text",
validateStatus: () => true validateStatus: () => true
@@ -703,9 +703,6 @@ export const pkiAcmeServiceFactory = ({
// Check and validate the CSR // Check and validate the CSR
const certificateRequest = extractCertificateRequestFromCSR(csr); const certificateRequest = extractCertificateRequestFromCSR(csr);
if (!certificateRequest.commonName) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" });
}
if ( if (
certificateRequest.subjectAlternativeNames?.some( certificateRequest.subjectAlternativeNames?.some(
(san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME (san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME
@@ -721,7 +718,7 @@ export const pkiAcmeServiceFactory = ({
const csrIdentifierValues = new Set( const csrIdentifierValues = new Set(
(certificateRequest.subjectAlternativeNames ?? []) (certificateRequest.subjectAlternativeNames ?? [])
.map((san) => san.value.toLowerCase()) .map((san) => san.value.toLowerCase())
.concat([certificateRequest.commonName.toLowerCase()]) .concat(certificateRequest.commonName ? [certificateRequest.commonName.toLowerCase()] : [])
); );
if ( if (
csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length || csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length ||
@@ -758,7 +755,8 @@ export const pkiAcmeServiceFactory = ({
} }
: // ttl is not used if notAfter is provided : // ttl is not used if notAfter is provided
({ ttl: "0d" } as const), ({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME enrollmentType: EnrollmentType.ACME,
allowEmptyCommonName: true
}); });
return { certificateId: result.certificateId }; return { certificateId: result.certificateId };
} }
@@ -1577,7 +1577,8 @@ export const internalCertificateAuthorityServiceFactory = ({
keyUsages, keyUsages,
extendedKeyUsages, extendedKeyUsages,
signatureAlgorithm, signatureAlgorithm,
keyAlgorithm keyAlgorithm,
allowEmptyCommonName
} = dto; } = dto;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
@@ -1716,12 +1717,18 @@ export const internalCertificateAuthorityServiceFactory = ({
const csrObj = new x509.Pkcs10CertificateRequest(csr); const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject); const dn = parseDistinguishedName(csrObj.subject);
const cn = commonName || dn.commonName; let cn = commonName || dn.commonName;
if (!cn) if ((allowEmptyCommonName ?? false) && !cn) {
// Notice: for modern TLS certificates, the CN is deprecated, many ACME clients will generate CSRs with without a CN
// we allow empty CN here to support ACME clients mostly. Since it's unclear what's the side effect of
// allowing empty CN for legacy PKI code, let's only do it if a true allowEmptyCommonName value is provided.
cn = "";
} else if (!cn) {
throw new BadRequestError({ throw new BadRequestError({
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
}); });
}
const { caPrivateKey, caSecret } = await getCaCredentials({ const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id, caId: ca.id,
@@ -164,6 +164,7 @@ export type TSignCertFromCaDTO =
keyAlgorithm?: string; keyAlgorithm?: string;
isFromProfile?: boolean; isFromProfile?: boolean;
profileId?: string; profileId?: string;
allowEmptyCommonName?: boolean;
} }
| ({ | ({
isInternal: false; isInternal: false;
@@ -183,6 +184,7 @@ export type TSignCertFromCaDTO =
keyAlgorithm?: string; keyAlgorithm?: string;
isFromProfile?: boolean; isFromProfile?: boolean;
profileId?: string; profileId?: string;
allowEmptyCommonName?: boolean;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = { export type TGetCaCertificateTemplatesDTO = {
@@ -511,7 +511,8 @@ export const certificateV3ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
enrollmentType, enrollmentType,
removeRootsFromChain removeRootsFromChain,
allowEmptyCommonName
}: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => { }: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => {
const profile = await validateProfileAndPermissions( const profile = await validateProfileAndPermissions(
profileId, profileId,
@@ -582,7 +583,8 @@ export const certificateV3ServiceFactory = ({
notAfter: normalizeDateForApi(notAfter), notAfter: normalizeDateForApi(notAfter),
signatureAlgorithm: effectiveSignatureAlgorithm, signatureAlgorithm: effectiveSignatureAlgorithm,
keyAlgorithm: effectiveKeyAlgorithm, keyAlgorithm: effectiveKeyAlgorithm,
isFromProfile: true isFromProfile: true,
allowEmptyCommonName
}); });
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
@@ -39,6 +39,7 @@ export type TSignCertificateFromProfileDTO = {
notAfter?: Date; notAfter?: Date;
enrollmentType: EnrollmentType; enrollmentType: EnrollmentType;
removeRootsFromChain?: boolean; removeRootsFromChain?: boolean;
allowEmptyCommonName?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TOrderCertificateFromProfileDTO = { export type TOrderCertificateFromProfileDTO = {