diff --git a/backend/bdd/features/pki/acme/internal-ca.feature b/backend/bdd/features/pki/acme/internal-ca.feature index c594f9a9f..6bd6ec4e9 100644 --- a/backend/bdd/features/pki/acme/internal-ca.feature +++ b/backend/bdd/features/pki/acme/internal-ca.feature @@ -1,13 +1,13 @@ Feature: Internal CA - Scenario Outline: CSR with SANs only + Scenario: CSR with SANs only 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" 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 Then I add names 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 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" - - Examples: - | names | - | {} | - | {"COMMON_NAME": ""} diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index def30b126..e084c8f0a 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -75,7 +75,7 @@ export const pkiAcmeChallengeServiceFactory = ({ const challengeResponse = await axios.get(challengeUrl.toString(), { // 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 - headers: { Host: host }, + headers: { Host: challenge.auth.identifierValue }, timeout: timeoutMs, responseType: "text", validateStatus: () => true diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 4f560ade7..9005d4bce 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -703,9 +703,6 @@ export const pkiAcmeServiceFactory = ({ // Check and validate the CSR const certificateRequest = extractCertificateRequestFromCSR(csr); - if (!certificateRequest.commonName) { - throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" }); - } if ( certificateRequest.subjectAlternativeNames?.some( (san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME @@ -721,7 +718,7 @@ export const pkiAcmeServiceFactory = ({ const csrIdentifierValues = new Set( (certificateRequest.subjectAlternativeNames ?? []) .map((san) => san.value.toLowerCase()) - .concat([certificateRequest.commonName.toLowerCase()]) + .concat(certificateRequest.commonName ? [certificateRequest.commonName.toLowerCase()] : []) ); if ( csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length || @@ -758,7 +755,8 @@ export const pkiAcmeServiceFactory = ({ } : // ttl is not used if notAfter is provided ({ ttl: "0d" } as const), - enrollmentType: EnrollmentType.ACME + enrollmentType: EnrollmentType.ACME, + allowEmptyCommonName: true }); return { certificateId: result.certificateId }; } diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index a7292e366..0b1609366 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -1577,7 +1577,8 @@ export const internalCertificateAuthorityServiceFactory = ({ keyUsages, extendedKeyUsages, signatureAlgorithm, - keyAlgorithm + keyAlgorithm, + allowEmptyCommonName } = dto; let collectionId = pkiCollectionId; @@ -1716,12 +1717,18 @@ export const internalCertificateAuthorityServiceFactory = ({ const csrObj = new x509.Pkcs10CertificateRequest(csr); 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({ message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" }); + } const { caPrivateKey, caSecret } = await getCaCredentials({ caId: ca.id, diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts index b4b037933..8d669f1d2 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -164,6 +164,7 @@ export type TSignCertFromCaDTO = keyAlgorithm?: string; isFromProfile?: boolean; profileId?: string; + allowEmptyCommonName?: boolean; } | ({ isInternal: false; @@ -183,6 +184,7 @@ export type TSignCertFromCaDTO = keyAlgorithm?: string; isFromProfile?: boolean; profileId?: string; + allowEmptyCommonName?: boolean; } & Omit); export type TGetCaCertificateTemplatesDTO = { diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index a537ddc06..1549c63a5 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -511,7 +511,8 @@ export const certificateV3ServiceFactory = ({ actorAuthMethod, actorOrgId, enrollmentType, - removeRootsFromChain + removeRootsFromChain, + allowEmptyCommonName }: TSignCertificateFromProfileDTO): Promise> => { const profile = await validateProfileAndPermissions( profileId, @@ -582,7 +583,8 @@ export const certificateV3ServiceFactory = ({ notAfter: normalizeDateForApi(notAfter), signatureAlgorithm: effectiveSignatureAlgorithm, keyAlgorithm: effectiveKeyAlgorithm, - isFromProfile: true + isFromProfile: true, + allowEmptyCommonName }); const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); diff --git a/backend/src/services/certificate-v3/certificate-v3-types.ts b/backend/src/services/certificate-v3/certificate-v3-types.ts index ab638c5ed..4f47e9099 100644 --- a/backend/src/services/certificate-v3/certificate-v3-types.ts +++ b/backend/src/services/certificate-v3/certificate-v3-types.ts @@ -39,6 +39,7 @@ export type TSignCertificateFromProfileDTO = { notAfter?: Date; enrollmentType: EnrollmentType; removeRootsFromChain?: boolean; + allowEmptyCommonName?: boolean; } & Omit; export type TOrderCertificateFromProfileDTO = {