diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index f08e882aa..7a73288cb 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -1,62 +1,115 @@ name: Release standalone docker image on: - push: - tags: - - "infisical/v*.*.*-postgres" + push: + tags: + - "infisical/v*.*.*-postgres" jobs: - infisical-tests: - name: Run tests before deployment - # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview - uses: ./.github/workflows/run-backend-tests.yml - infisical-standalone: - name: Build infisical standalone image postgres - runs-on: ubuntu-latest - needs: [infisical-tests] - steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" - - name: ☁️ Checkout source - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: 📦 Install dependencies to test all dependencies - run: npm ci --only-production - working-directory: backend - - name: version output - run: | - echo "Output Value: ${{ steps.version.outputs.major }}" - echo "Output Value: ${{ steps.version.outputs.minor }}" - echo "Output Value: ${{ steps.version.outputs.patch }}" - echo "Output Value: ${{ steps.version.outputs.version }}" - echo "Output Value: ${{ steps.version.outputs.version_type }}" - echo "Output Value: ${{ steps.version.outputs.increment }}" - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - name: 📦 Build backend and export to Docker - uses: depot/build-push-action@v1 - with: - project: 64mmf0n610 - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - push: true - context: . - tags: | - infisical/infisical:latest-postgres - infisical/infisical:${{ steps.commit.outputs.short }} - infisical/infisical:${{ steps.extract_version.outputs.version }} - platforms: linux/amd64,linux/arm64 - file: Dockerfile.standalone-infisical - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} + infisical-tests: + name: Run tests before deployment + # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview + uses: ./.github/workflows/run-backend-tests.yml + + infisical-standalone: + name: Build infisical standalone image postgres + runs-on: ubuntu-latest + needs: [infisical-tests] + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: version output + run: | + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + tags: | + infisical/infisical:latest-postgres + infisical/infisical:${{ steps.commit.outputs.short }} + infisical/infisical:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + file: Dockerfile.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} + + infisical-fips-standalone: + name: Build infisical standalone image postgres + runs-on: ubuntu-latest + needs: [infisical-tests] + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: version output + run: | + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + tags: | + infisical/infisical-fips:latest-postgres + infisical/infisical-fips:${{ steps.commit.outputs.short }} + infisical/infisical-fips:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + file: Dockerfile.fips.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} diff --git a/.infisicalignore b/.infisicalignore index b7fc38b35..4c19af70c 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -6,3 +6,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S docs/self-hosting/configuration/envars.mdx:generic-api-key:106 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 docs/mint.json:generic-api-key:651 +backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134 diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical new file mode 100644 index 000000000..53c24db5d --- /dev/null +++ b/Dockerfile.fips.standalone-infisical @@ -0,0 +1,167 @@ +ARG POSTHOG_HOST=https://app.posthog.com +ARG POSTHOG_API_KEY=posthog-api-key +ARG INTERCOM_ID=intercom-id +ARG CAPTCHA_SITE_KEY=captcha-site-key + +FROM --platform=linux/amd64 node:20-slim AS base + +FROM base AS frontend-dependencies +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ + +# Install dependencies +RUN npm ci --only-production --ignore-scripts + +# Rebuild the source code only when needed +FROM --platform=linux/amd64 base AS frontend-builder +WORKDIR /app + +# Copy dependencies +COPY --from=frontend-dependencies /app/node_modules ./node_modules +# Copy all files +COPY /frontend . + +ENV NODE_ENV production +ENV NEXT_PUBLIC_ENV production +ARG POSTHOG_HOST +ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY +ARG INTERCOM_ID +ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID +ARG INFISICAL_PLATFORM_VERSION +ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ARG CAPTCHA_SITE_KEY +ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY + +# Build +RUN npm run build + +# Production image +FROM --platform=linux/amd64 base AS frontend-runner +WORKDIR /app + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images +VOLUME /app/.next/cache/images + +COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts +COPY --from=frontend-builder /app/public ./public +RUN chown non-root-user:nodejs ./public/data + +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./ +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static + +USER non-root-user + +ENV NEXT_TELEMETRY_DISABLED 1 + +## +## BACKEND +## +FROM --platform=linux/amd64 base AS backend-build + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +WORKDIR /app + +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/package*.json ./ +RUN npm ci --only-production + +COPY /backend . +COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh +RUN npm i -D tsconfig-paths +RUN npm run build + +# Production stage +FROM --platform=linux/amd64 base AS backend-runner + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +WORKDIR /app + +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/package*.json ./ +RUN npm ci --only-production + +COPY --from=backend-build /app . + +RUN mkdir frontend-build + +# Production stage +FROM --platform=linux/amd64 base AS production + +# Install necessary packages +RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Infisical CLI +RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \ + && apt-get update && apt-get install -y infisical=0.31.1 \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +# Give non-root-user permission to update SSL certs +RUN chown -R non-root-user /etc/ssl/certs +RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt +RUN chmod -R u+rwx /etc/ssl/certs +RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt +RUN chown non-root-user /usr/sbin/update-ca-certificates +RUN chmod u+rx /usr/sbin/update-ca-certificates + +## set pre baked keys +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ + BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY +ARG INTERCOM_ID=intercom-id +ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ + BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG CAPTCHA_SITE_KEY +ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \ + BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY + +WORKDIR / + +COPY --from=backend-runner /app /backend + +COPY --from=frontend-runner /app ./backend/frontend-build + +ENV PORT 8080 +ENV HOST=0.0.0.0 +ENV HTTPS_ENABLED false +ENV NODE_ENV production +ENV STANDALONE_BUILD true +ENV STANDALONE_MODE true +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +WORKDIR /backend + +ENV TELEMETRY_ENABLED true + +EXPOSE 8080 +EXPOSE 443 + +USER non-root-user + +CMD ["./standalone-entrypoint.sh"] \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index ef9f4e0d3..d55521f5b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -55,7 +55,6 @@ "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", - "graphene-pk11": "^2.3.6", "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", @@ -84,6 +83,7 @@ "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", + "pkcs11js": "^2.1.6", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", @@ -121,6 +121,7 @@ "@types/passport-google-oauth20": "^2.0.14", "@types/pg": "^8.10.9", "@types/picomatch": "^2.3.3", + "@types/pkcs11js": "^1.0.4", "@types/prompt-sync": "^4.2.3", "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", @@ -8831,6 +8832,17 @@ "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", "dev": true }, + "node_modules/@types/pkcs11js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/pkcs11js/-/pkcs11js-1.0.4.tgz", + "integrity": "sha512-Pkq8VbwZZv7o/6ODFOhxw0s0M8J4ucg4/I4V1dSCn8tUwWgIKIYzuV4Pp2fYuir81DgQXAF5TpGyhBMjJ3FjFw==", + "deprecated": "This is a stub types definition for pkcs11js (https://github.com/PeculiarVentures/pkcs11js). pkcs11js provides its own type definitions, so you don't need @types/pkcs11js installed!", + "dev": true, + "license": "MIT", + "dependencies": { + "pkcs11js": "*" + } + }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -13557,23 +13569,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/graphene-pk11": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/graphene-pk11/-/graphene-pk11-2.3.6.tgz", - "integrity": "sha512-ol9Pf7XDv5UTjh1DPqtmQVZQqUheiXBzQVXQWRCLWq78+brKQB0Kum/s0NGEcsd/5NQQG8MFA2U/KNujEoC1fQ==", - "license": "MIT", - "dependencies": { - "pkcs11js": "^2.1.6", - "tslib": "^2.7.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/PeculiarVentures" - } - }, "node_modules/graphql": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", diff --git a/backend/package.json b/backend/package.json index 0278689d3..240b7db2c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -84,6 +84,7 @@ "@types/passport-google-oauth20": "^2.0.14", "@types/pg": "^8.10.9", "@types/picomatch": "^2.3.3", + "@types/pkcs11js": "^1.0.4", "@types/prompt-sync": "^4.2.3", "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", @@ -160,7 +161,6 @@ "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", - "graphene-pk11": "^2.3.6", "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", @@ -189,6 +189,7 @@ "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", + "pkcs11js": "^2.1.6", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 59a50f51f..f91f9a004 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,4 +1,4 @@ -import * as grapheneLib from "graphene-pk11"; +import * as pkcs11js from "pkcs11js"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; @@ -8,26 +8,47 @@ import { HsmModule } from "./hsm-types"; export const initializeHsmModule = () => { const appCfg = getConfig(); - let module: grapheneLib.Module | null = null; + // Create a new instance of PKCS11 module + const pkcs11 = new pkcs11js.PKCS11(); + let isInitialized = false; const initialize = () => { if (!appCfg.isHsmConfigured) { return; } - module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "InfisicalHSM"); - module.initialize(); - logger.info("PKCS#11 module initialized"); - }; + try { + // Load the PKCS#11 module + pkcs11.load(appCfg.HSM_LIB_PATH!); - const finalize = () => { - if (module) { - module.finalize(); - logger.info("PKCS#11 module finalized"); + // Initialize the module + pkcs11.C_Initialize(); + isInitialized = true; + + logger.info("PKCS#11 module initialized"); + } catch (err) { + logger.error("Failed to initialize PKCS#11 module:", err); + throw err; } }; - const getModule = (): HsmModule => ({ module, graphene: grapheneLib }); + const finalize = () => { + if (isInitialized) { + try { + pkcs11.C_Finalize(); + isInitialized = false; + logger.info("PKCS#11 module finalized"); + } catch (err) { + logger.error("Failed to finalize PKCS#11 module:", err); + throw err; + } + } + }; + + const getModule = (): HsmModule => ({ + pkcs11, + isInitialized + }); return { initialize, diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index fe273b3ee..73c4b279c 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,277 +1,339 @@ -import grapheneLib from "graphene-pk11"; +import pkcs11js from "pkcs11js"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; -import { Lock } from "@app/lib/red-lock"; -import { HsmModule, RequiredMechanisms } from "./hsm-types"; +import { HsmKeyType, HsmModule } from "./hsm-types"; type THsmServiceFactoryDep = { hsmModule: HsmModule; - keyStore: Pick; }; -const HSM_SESSION_WAIT_KEY = "wait_till_hsm_session_ready"; - -const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; -const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding - export type THsmServiceFactory = ReturnType; type SyncOrAsync = T | Promise; -type SessionCallback = (session: grapheneLib.Session) => SyncOrAsync; +type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: THsmServiceFactoryDep) => { +export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => { const appCfg = getConfig(); - // Constants for buffer structure - const IV_LENGTH = 12; - const TAG_LENGTH = 16; + // Constants for buffer structures + const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc + const BLOCK_SIZE = 16; + const HMAC_SIZE = 32; const $withSession = async (callbackWithSession: SessionCallback): Promise => { const RETRY_INTERVAL = 300; // 300ms between attempts const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time - let session: grapheneLib.Session | null = null; - let lock: Lock | null = null; + let sessionHandle: pkcs11js.Handle | null = null; const removeSession = () => { - if (session) { - session.logout(); - session.close(); - session = null; + if (sessionHandle !== null) { + try { + pkcs11.C_Logout(sessionHandle); + pkcs11.C_CloseSession(sessionHandle); + logger.info("HSM: Terminated session successfully"); + } catch (error) { + logger.error("Error during session cleanup:", error); + } finally { + sessionHandle = null; + } } }; try { - if (!module) { + if (!pkcs11 || !isInitialized) { throw new Error("PKCS#11 module is not initialized"); } - // Create new session - const slot = module.getSlots(appCfg.HSM_SLOT); - // eslint-disable-next-line no-bitwise - if (!(slot.flags & graphene.SlotFlag.TOKEN_PRESENT)) { - throw new Error("Slot is not initialized"); + // Get slot list + let slots: pkcs11js.Handle[]; + try { + slots = pkcs11.C_GetSlotList(false); // false to get all slots + } catch (error) { + throw new Error(`Failed to get slot list: ${(error as Error)?.message}`); } - lock = await keyStore.acquireLock(["HSM_SESSION_LOCK"], 10_000, { retryCount: 3 }).catch(() => null); - - if (!lock) { - await keyStore.waitTillReady({ - key: HSM_SESSION_WAIT_KEY, - keyCheckCb: (val) => val === "true", - waitingCb: () => logger.info("HSM Lock: Waiting for session to be available...") - }); + if (slots.length === 0) { + throw new Error("No slots available"); } + if (appCfg.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`); + } + + const slotId = slots[appCfg.HSM_SLOT]; + const startTime = Date.now(); while (Date.now() - startTime < MAX_TIMEOUT) { try { + // Open session // eslint-disable-next-line no-bitwise - session = slot.open(graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION); - session.login(appCfg.HSM_PIN!); - // session.login("4311"); - break; - } catch (error) { - if ((error as Error)?.message !== USER_ALREADY_LOGGED_IN_ERROR) { - throw error; + sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION); + + // Login + try { + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN); + logger.info("HSM: Successfully authenticated"); + break; + } catch (error) { + if (error instanceof pkcs11js.Pkcs11Error) { + // Handle specific error cases + if (error.code === pkcs11js.CKR_PIN_INCORRECT) { + logger.error(error, `Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); + throw new Error("Incorrect HSM Pin detected. Please check the HSM configuration."); + } + + if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { + logger.warn("HSM session already logged in"); + } + } + throw error; // Re-throw other errors } - logger.warn("HSM session already logged in"); + } catch (error) { + logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`); + + if (sessionHandle !== null) { + try { + pkcs11.C_CloseSession(sessionHandle); + } catch (closeError) { + logger.error("Error closing failed session:", closeError); + } + sessionHandle = null; + } + + // Wait before retrying + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, RETRY_INTERVAL); + }); + } + } + + if (sessionHandle === null) { + throw new Error("Failed to open session after maximum retries"); + } + + // Execute callback with session handle + const result = await callbackWithSession(sessionHandle); + removeSession(); + return result; + } catch (error) { + logger.error("Error in HSM session handling:", error); + throw error; + } finally { + // Ensure cleanup + removeSession(); + } + }; + + const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { + const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL; + const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES; + + const template = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: keyType }, + { type: pkcs11js.CKA_LABEL, value: label } + ]; + + try { + // Initialize search + pkcs11.C_FindObjectsInit(sessionHandle, template); + + try { + // Find first matching object + const handles = pkcs11.C_FindObjects(sessionHandle, 1); + + if (handles.length === 0) { + throw new Error("Failed to find master key"); } - logger.warn(`HSM: No session available. Waiting for session to be available... [retry=${RETRY_INTERVAL}ms]`); - - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - setTimeout(resolve, RETRY_INTERVAL); - }); + return handles[0]; // Return the key handle + } finally { + // Always finalize the search operation + pkcs11.C_FindObjectsFinal(sessionHandle); } - - if (!session) { - throw new Error("Failed to open session"); - } - - // Execute the callback and await its result (works for both sync and async) - const result = await callbackWithSession(session); - - if (session) { - removeSession(); - await keyStore.setItemWithExpiry(HSM_SESSION_WAIT_KEY, 10, "true"); - } - - return result; - } finally { - // Clean up session if it was created - try { - removeSession(); - } catch (error) { - logger.error(error, "Error cleaning up HSM session:"); - } - - await lock?.release(); + } catch (error) { + logger.error("Error finding master key:", error); + return null; } }; - const $findMasterKey = (session: grapheneLib.Session) => { - // Find the master key (root key) - const template = { - class: graphene.ObjectClass.SECRET_KEY, - keyType: graphene.KeyType.AES, - label: appCfg.HSM_KEY_LABEL - } as grapheneLib.ITemplate; - - const key = session.find(template).items(0); - - if (!key) { - throw new Error("Failed to find master key"); - } - - return key; - }; - - const $generateAndWrapKey = (session: grapheneLib.Session) => { - const masterKey = $findMasterKey(session); - - // Generate a new session key for encryption - const sessionKey = session.generateKey(graphene.KeyGenMechanism.AES, { - class: graphene.ObjectClass.SECRET_KEY, - keyType: graphene.KeyType.AES, - token: false, // Session-only key - sensitive: true, - extractable: true, // Must be true to allow wrapping - encrypt: true, - decrypt: true, - valueLen: 32 // 256-bit key - } as grapheneLib.ITemplate); - - // Wrap the session key with master key - const wrappingMech = { name: "AES_KEY_WRAP", params: null }; - const wrappedKey = session.wrapKey( - wrappingMech, - new graphene.Key(masterKey).toType(), - new graphene.Key(sessionKey).toType() - ); - - return { wrappedKey, sessionKey }; - }; - - const $unwrapKey = (session: grapheneLib.Session, wrappedKey: Buffer) => { - const masterKey = $findMasterKey(session); - - // Absolute minimal template - let HSM set most attributes - const unwrapTemplate = { - class: graphene.ObjectClass.SECRET_KEY, - keyType: graphene.KeyType.AES - } as grapheneLib.ITemplate; - - const unwrappingMech = { - name: "AES_KEY_WRAP", - params: null - } as grapheneLib.MechanismType; - - return session.unwrapKey(unwrappingMech, new graphene.Key(masterKey).toType(), wrappedKey, unwrapTemplate); - }; - - const $keyExists = (session: grapheneLib.Session): boolean => { + const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => { try { - const key = $findMasterKey(session); + const key = $findKey(session, type); // items(0) will throw an error if no items are found // Return true only if we got a valid object with handle - return key && typeof key.handle !== "undefined"; + return !!key && key.length > 0; } catch (error) { // If items(0) throws, it means no key was found // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call - if ((error as any).message?.includes("CKR_OBJECT_HANDLE_INVALID")) { - return false; - } logger.error(error, "Error checking for HSM key presence"); + + if (error instanceof pkcs11js.Pkcs11Error) { + if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) { + return false; + } + } + return false; } }; const encrypt: { - (data: Buffer, providedSession: grapheneLib.Session): Promise; + (data: Buffer, providedSession: pkcs11js.Handle): Promise; (data: Buffer): Promise; - } = async (data: Buffer, providedSession?: grapheneLib.Session) => { - if (!module) { + } = async (data: Buffer, providedSession?: pkcs11js.Handle) => { + if (!pkcs11 || !isInitialized) { throw new Error("PKCS#11 module is not initialized"); } - const $performEncryption = (s: grapheneLib.Session) => { - // Generate IV for encryption - const iv = s.generateRandom(IV_LENGTH); + const $performEncryption = (sessionHandle: pkcs11js.Handle) => { + try { + const aesKey = $findKey(sessionHandle, HsmKeyType.AES); + if (!aesKey) { + throw new Error("AES key not found"); + } - // Generate and wrap a new session key - const { wrappedKey, sessionKey } = $generateAndWrapKey(s); + const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); + if (!hmacKey) { + throw new Error("HMAC key not found"); + } - const alg = { - name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) - } as grapheneLib.IAlgorithm; + const iv = Buffer.alloc(IV_LENGTH); + pkcs11.C_GenerateRandom(sessionHandle, iv); - const cipher = s.createCipher(alg, new graphene.Key(sessionKey).toType()); + const encryptMechanism = { + mechanism: pkcs11js.CKM_AES_CBC_PAD, + parameter: iv + }; - // Calculate the output buffer size based on input length - // GCM adds a 16-byte auth tag, so we need input length + 16 - const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); - const encryptedData = cipher.once(data, outputBuffer); + pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey); - // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] - return Buffer.concat([wrappedKey, iv, encryptedData]); + // Calculate max buffer size (input length + potential full block of padding) + const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE; + const tempBuffer = Buffer.alloc(maxEncryptedLength); + + // First call to get the actual length + const encryptedLength = pkcs11.C_Encrypt(sessionHandle, data, tempBuffer); + + // Create a copy of the encrypted data using the actual length + const encryptedData = Buffer.from(tempBuffer.slice(0, encryptedLength.length || 16)); + + // Initialize HMAC + const hmacMechanism = { + mechanism: pkcs11js.CKM_SHA256_HMAC + }; + + pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey); + + // Sign the IV and encrypted data + pkcs11.C_SignUpdate(sessionHandle, iv); + pkcs11.C_SignUpdate(sessionHandle, encryptedData); + + // Get the HMAC + const hmac = Buffer.alloc(HMAC_SIZE); + pkcs11.C_SignFinal(sessionHandle, hmac); + + // Combine encrypted data and HMAC [Encrypted Data | HMAC] + const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length); + encryptedData.copy(finalBuffer); + hmac.copy(finalBuffer, encryptedData.length); + + return Buffer.concat([iv, finalBuffer]); + } catch (error) { + logger.error("Encryption error:", error); + throw new Error(`Encryption failed: ${(error as Error)?.message}`); + } }; if (providedSession) { return $performEncryption(providedSession); } - const encrypted = await $withSession($performEncryption); - - return encrypted; + const result = await $withSession($performEncryption); + return result; }; const decrypt: { - (encryptedBlob: Buffer, providedSession: grapheneLib.Session): Promise; + (encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise; (encryptedBlob: Buffer): Promise; - } = async (encryptedBlob: Buffer, providedSession?: grapheneLib.Session) => { - if (!module) { + } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => { + if (!isInitialized) { throw new Error("HSM service not initialized"); } - const $performDecryption = (s: grapheneLib.Session) => { - const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH); - const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH); - const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH); + const $performDecryption = (sessionHandle: pkcs11js.Handle) => { + try { + // structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)] + const iv = encryptedBlob.subarray(0, IV_LENGTH); + const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH); - // Unwrap the session key - const sessionKey = $unwrapKey(s, wrappedKey); + // Split encrypted data and HMAC + const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC - const algo = { - name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) - }; + const encryptedData = encryptedDataWithHmac.slice(0, -HMAC_SIZE); // Everything except last 32 bytes - const decipher = s.createDecipher(algo, new graphene.Key(sessionKey).toType()); - const outputBuffer = Buffer.alloc(ciphertext.length); + // Find the keys + const aesKey = $findKey(sessionHandle, HsmKeyType.AES); + if (!aesKey) { + throw new Error("AES key not found"); + } - // Extract wrapped key, IV, and ciphertext - return decipher.once(ciphertext, outputBuffer); + const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); + if (!hmacKey) { + throw new Error("HMAC key not found"); + } + + // Verify HMAC first + const hmacMechanism = { + mechanism: pkcs11js.CKM_SHA256_HMAC + }; + + pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey); + pkcs11.C_VerifyUpdate(sessionHandle, iv); + pkcs11.C_VerifyUpdate(sessionHandle, encryptedData); + + try { + pkcs11.C_VerifyFinal(sessionHandle, hmac); + } catch (error) { + throw new Error("Decryption failed"); // Generic error for failed verification + } + + // Only decrypt if verification passed + const decryptMechanism = { + mechanism: pkcs11js.CKM_AES_CBC_PAD, + parameter: iv + }; + + pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey); + + const tempBuffer = Buffer.alloc(encryptedData.length); + const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer); + + // Create a new buffer from the decrypted data + return Buffer.from(decryptedData); + } catch (error) { + logger.error("Decryption error:", error); + throw new Error(`Decryption failed: ${(error as Error)?.message}`); + } }; if (providedSession) { return $performDecryption(providedSession); } - const decrypted = await $withSession($performDecryption); - return decrypted; + const result = await $withSession($performDecryption); + return result; }; // We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device. - const $testPkcs11Module = async (session: grapheneLib.Session) => { + const $testPkcs11Module = async (session: pkcs11js.Handle) => { try { - if (!module) { + if (!isInitialized) { throw new Error("HSM service not initialized"); } @@ -279,11 +341,15 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: throw new Error("Session not initialized"); } - const randomData = session.generateRandom(256); - const encryptedData = await encrypt(Buffer.from(randomData), session); + const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500)); + + const encryptedData = await encrypt(randomData, session); const decryptedData = await decrypt(encryptedData, session); - if (Buffer.from(randomData).toString("hex") !== Buffer.from(decryptedData).toString("hex")) { + const randomDataHex = randomData.toString("hex"); + const decryptedDataHex = decryptedData.toString("hex"); + + if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) { throw new Error("Decrypted data does not match original data"); } @@ -295,7 +361,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: }; const isActive = async () => { - if (!module || !appCfg.isHsmConfigured) { + if (!isInitialized || !appCfg.isHsmConfigured) { return false; } @@ -304,62 +370,94 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: try { pkcs11TestPassed = await $withSession($testPkcs11Module); } catch (err) { - logger.error(err, "isActive: Error testing PKCS#11 module"); + logger.error(err, "HSM: Error testing PKCS#11 module"); } - return appCfg.isHsmConfigured && module !== null && pkcs11TestPassed; + return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed; }; const startService = async () => { - if (!appCfg.isHsmConfigured || !module) return; + if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return; try { - await $withSession(async (session) => { + await $withSession(async (sessionHandle) => { // Check if master key exists, create if not - if (!$keyExists(session)) { - // Generate 256-bit AES master key with persistent storage - session.generateKey(graphene.KeyGenMechanism.AES, { - class: graphene.ObjectClass.SECRET_KEY, - token: true, - valueLen: 256 / 8, - keyType: graphene.KeyType.AES, - label: appCfg.HSM_KEY_LABEL, - derive: true, // Enable key derivation - extractable: false, - sensitive: true, - private: true - }); + + const genericAttributes = [ + { type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage + { type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted + { type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value + { type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication + ]; + + if (!$keyExists(sessionHandle, HsmKeyType.AES)) { + // Template for generating 256-bit AES master key + const keyTemplate = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES }, + { type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 }, + { type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! }, + { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption + { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption + ...genericAttributes + ]; + + // Generate the key + pkcs11.C_GenerateKey( + sessionHandle, + { + mechanism: pkcs11js.CKM_AES_KEY_GEN + }, + keyTemplate + ); + logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); } - // Verify HSM supports required mechanisms - const mechs = session.slot.getMechanisms(); - const mechNames: string[] = []; + // Check if HMAC key exists, create if not + if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) { + const hmacKeyTemplate = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET }, + { type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 }, // 256-bit key + { type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, + { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing + { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification + ...genericAttributes + ]; - // eslint-disable-next-line no-plusplus - for (let i = 0; i < mechs.length; i++) { - mechNames.push(mechs.items(i).name); + // Generate the HMAC key + pkcs11.C_GenerateKey( + sessionHandle, + { + mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN + }, + hmacKeyTemplate + ); + + logger.info(`HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); } - const hasAesGcm = mechNames.includes(RequiredMechanisms.AesGcm); - const hasAesKeyWrap = mechNames.includes(RequiredMechanisms.AesKeyWrap); + // Get slot info to check supported mechanisms + const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID; + const mechanisms = pkcs11.C_GetMechanismList(slotId); - if (!hasAesGcm) { - throw new Error(`Required mechanism ${RequiredMechanisms.AesGcm} not supported by HSM`); - } - if (!hasAesKeyWrap) { - throw new Error(`Required mechanism ${RequiredMechanisms.AesKeyWrap} not supported by HSM`); + // Check for AES CBC PAD support + const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD); + + if (!hasAesCbc) { + throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`); } - const testPassed = await $testPkcs11Module(session); + // Run test encryption/decryption + const testPassed = await $testPkcs11Module(sessionHandle); - // Run a test to verify module is working if (!testPassed) { throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured."); } }); } catch (error) { - logger.error(error, "Error initializing HSM service"); + logger.error("Error initializing HSM service:", error); throw error; } }; diff --git a/backend/src/ee/services/hsm/hsm-types.ts b/backend/src/ee/services/hsm/hsm-types.ts index e7c33ffda..b688147f5 100644 --- a/backend/src/ee/services/hsm/hsm-types.ts +++ b/backend/src/ee/services/hsm/hsm-types.ts @@ -1,11 +1,11 @@ -import * as grapheneLib from "graphene-pk11"; +import pkcs11js from "pkcs11js"; export type HsmModule = { - module: grapheneLib.Module | null; - graphene: typeof grapheneLib; + pkcs11: pkcs11js.PKCS11; + isInitialized: boolean; }; -export enum RequiredMechanisms { - AesGcm = "AES_GCM", - AesKeyWrap = "AES_KEY_WRAP" +export enum HsmKeyType { + AES = "AES", + HMAC = "hmac" } diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index cb41ebd72..72e0c0d05 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -166,35 +166,10 @@ const envSchema = z ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), // HSM - HSM_LIB_PATH: zpStr( - z - .string() - .optional() - .transform((val) => { - if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so"; - return val; - }) - ), - HSM_PIN: zpStr( - z - .string() - .optional() - .transform((val) => { - if (process.env.NODE_ENV === "development") return "1234"; - return val; - }) - ), - HSM_KEY_LABEL: zpStr( - z - .string() - .optional() - .transform((val) => { - if (process.env.NODE_ENV === "development") return "auth-app"; - return val; - }) - ), - HSM_SLOT: z.coerce.number().optional().default(0), - HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM")) + HSM_LIB_PATH: zpStr(z.string().optional()), + HSM_PIN: zpStr(z.string().optional()), + HSM_KEY_LABEL: zpStr(z.string().optional()), + HSM_SLOT: z.coerce.number().optional().default(0) }) // To ensure that basic encryption is always possible. .refine( @@ -218,11 +193,7 @@ const envSchema = z Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), isHsmConfigured: - Boolean(data.HSM_LIB_PATH) && - Boolean(data.HSM_PIN) && - Boolean(data.HSM_KEY_LABEL) && - Boolean(data.HSM_MECHANISM) && - data.HSM_SLOT !== undefined, + Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 065b3f617..88bfaaaae 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -363,8 +363,7 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - hsmModule, - keyStore + hsmModule }); const kmsService = kmsServiceFactory({