diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 49609e8c9..a55f2edff 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -8,6 +8,7 @@ import { ForbiddenError } from "@casl/ability"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; +import { verifyOfflineLicense } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -26,6 +27,7 @@ import { TFeatureSet, TGetOrgBillInfoDTO, TGetOrgTaxIdDTO, + TOfflineLicenseContents, TOrgInvoiceDTO, TOrgLicensesDTO, TOrgPlanDTO, @@ -96,6 +98,36 @@ export const licenseServiceFactory = ({ } return; } + + if (appCfg.LICENSE_KEY_OFFLINE) { + let isValidOfflineLicense = true; + const contents: TOfflineLicenseContents = JSON.parse( + Buffer.from(appCfg.LICENSE_KEY_OFFLINE, "base64").toString("utf8") + ); + const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature); + + if (!isVerified) { + isValidOfflineLicense = false; + logger.warn(`Infisical EE offline license verification failed`); + } + + if (contents.license.terminatesAt) { + const terminationDate = new Date(contents.license.terminatesAt); + if (terminationDate < new Date()) { + isValidOfflineLicense = false; + logger.warn(`Infisical EE offline license has expired`); + } + } + + if (isValidOfflineLicense) { + onPremFeatures = contents.license.features; + instanceType = InstanceType.EnterpriseOnPrem; + logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`); + isValidLicense = true; + return; + } + } + // this means this is self hosted oss version // else it would reach catch statement isValidLicense = true; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 80f422380..f8ed8aff3 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -6,6 +6,21 @@ export enum InstanceType { Cloud = "cloud" } +export type TOfflineLicenseContents = { + license: TOfflineLicense; + signature: string; +}; + +export type TOfflineLicense = { + issuedTo: string; + licenseId: string; + customerId: string | null; + issuedAt: string; + expiresAt: string | null; + terminatesAt: string | null; + features: TFeatureSet; +}; + export type TFeatureSet = { _id: null; slug: null; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 84d772e2e..6b7c02f6b 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -106,6 +106,7 @@ const envSchema = z LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), LICENSE_SERVER_KEY: zpStr(z.string().optional()), LICENSE_KEY: zpStr(z.string().optional()), + LICENSE_KEY_OFFLINE: zpStr(z.string().optional()), // GENERIC STANDALONE_MODE: z diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts index 9d7f4e886..db3d91fc8 100644 --- a/backend/src/lib/crypto/index.ts +++ b/backend/src/lib/crypto/index.ts @@ -17,4 +17,5 @@ export { decryptSecrets, decryptSecretVersions } from "./secret-encryption"; +export { verifyOfflineLicense } from "./signing"; export { generateSrpServerKey, srpCheckClientProof } from "./srp"; diff --git a/backend/src/lib/crypto/license_public_key.pem b/backend/src/lib/crypto/license_public_key.pem new file mode 100644 index 000000000..0cda06f3c --- /dev/null +++ b/backend/src/lib/crypto/license_public_key.pem @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEApchBY3BXTu4zWGBguB7nM/pjpVLY3V7VGZOAxmR5ueQTJOwiGM13 +5HN3EM9fDlQnZu9VSc0OFqRM/bUeUaI1oLPE6WzTHjdHyKjDI/S+TLx3VGEsvhM1 +uukZpYX+3KX2w4wzRHBaBWyglFy0CVNth9UJhhpD+KKfv7dzcRmsbyoUWi9wGfJu +wLYCwaCwZRXIt1sLGmMncPz14vfwdnm2a5Tj1Jbt0GTyBl+1/ZqLbO6SsslLg2G+ +o7FfGS9z8OUTkvDdu16qxL+p2wCEFZMnOz5BB4oakuT2gS9iOO2l5AOPcT4WzPzy +PYbX3d7cN9BkOY9I5z0cX4wzqHjQTvGNLQIDAQAB +-----END RSA PUBLIC KEY----- \ No newline at end of file diff --git a/backend/src/lib/crypto/signing.ts b/backend/src/lib/crypto/signing.ts new file mode 100644 index 000000000..36c858715 --- /dev/null +++ b/backend/src/lib/crypto/signing.ts @@ -0,0 +1,22 @@ +import crypto, { KeyObject } from "crypto"; +import fs from "fs/promises"; +import path from "path"; + +export const verifySignature = (data: string, signature: Buffer, publicKey: KeyObject) => { + const verify = crypto.createVerify("SHA256"); + verify.update(data); + verify.end(); + return verify.verify(publicKey, signature); +}; + +export const verifyOfflineLicense = async (licenseContents: string, signature: string) => { + const publicKeyPem = await fs.readFile(path.join(__dirname, "license_public_key.pem"), "utf8"); + + const publicKey = crypto.createPublicKey({ + key: publicKeyPem, + format: "pem", + type: "pkcs1" + }); + + return verifySignature(licenseContents, Buffer.from(signature, "base64"), publicKey); +};