Generating SRI Hashes in Your Build Pipeline

Computing integrity attribute values by hand does not scale past a handful of static assets — build pipelines that emit dozens of hashed chunks need automated SRI generation wired into the same step that writes fingerprinted filenames.

Symptom and Decision Framing

If your deployment process includes any of the following, you need pipeline-integrated SRI generation rather than manual hash computation:

  • More than five independently cached asset files per page
  • Code splitting (dynamic import()) that emits an unpredictable number of chunks per build
  • A CI/CD workflow that builds and deploys without human review of individual file names
  • Asset filenames that change on every meaningful code edit (content-addressed hashing)

Manual openssl commands remain useful for spot-checking individual files and for verifying that a deployed asset matches its build-time hash, but they cannot reliably track every chunk emitted by a modern bundler. The SRI validation reference explains how browsers use these hashes; this guide focuses entirely on generating them correctly during the build.

Concept Clarification: What Gets Hashed

SRI hashes are computed over the final bytes of the output file as it will be served to the browser, after all bundler transforms (minification, tree-shaking, scope-hoisting) but before transport encoding (gzip/Brotli). This means:

  • Compute the hash after the bundler writes the file to disk.
  • Do not compute the hash on source files, on intermediate representations, or on the compressed version of the file.
  • If your CDN applies further content transforms (auto-minification, whitespace removal), those transforms change the bytes and therefore invalidate the hash. Disable content transforms on CDN for SRI-protected paths (see debugging SRI validation failures for the full diagnosis workflow).

The fingerprint in the filename (e.g., main.a1b2c3d4.js — 8 hex chars by default, or 12–16 for large monorepos with many chunks) is derived from the same content hash used for cache key architecture, but it is truncated and hex-encoded. The SRI hash is the full SHA-384 (or SHA-512) digest base64-encoded — a completely different representation of the same byte stream.

Comparison: Generation Approaches

Approach Toolchain fit Dynamic chunks CI friction Hash algorithm control
OpenSSL one-liner (manual) Any None — manual per file High Full
webpack-subresource-integrity Webpack 5 Full (patches runtime) None hashFuncNames array
vite-plugin-sri3 Vite 5 / Rollup 4 Entry + static chunks None algorithms array
Custom Rollup generateBundle hook Rollup 4 Entry + static chunks Low (few lines) Full
Node.js manifest script (post-build) Any (reads manifest.json) Limited by manifest entries Low Full
Build-time SRI generation Source files enter the bundler, which emits hashed output files. A hash computation step reads those files and writes integrity values into a manifest, which the HTML template reads to inject integrity attributes. Source src/*.ts, *.css Bundler minify + split Hashed Output main.a1b2c3d4.js styles.b3c4d5e6.css SRI Compute SHA-384 → base64 per output file Manifest file → integrity mapping HTML Output integrity= injected crossorigin=anon Plugin-automated path (webpack-subresource-integrity / vite-plugin-sri3) or manual Node.js script reading manifest.json post-build
Build-time SRI generation: the bundler emits hashed output files, a hash computation step produces base64 digests, and those values are written into the HTML output via a manifest.

OpenSSL One-Liners

For individual files, openssl is the fastest path to a correct hash:

# SHA-256 (acceptable but prefer SHA-384 or SHA-512 for SRI)
openssl dgst -sha256 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A

# SHA-384 (recommended for new deployments)
openssl dgst -sha384 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A

# SHA-512
openssl dgst -sha512 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A

# Emit the full integrity attribute value ready to paste into HTML
printf "sha384-"; openssl dgst -sha384 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A

# Hash every JS file in the dist directory
find dist/assets -name "*.js" -exec sh -c \
  'printf "sha384-"; openssl dgst -sha384 -binary "$1" | openssl base64 -A; echo " $1"' \
  _ {} \;

These commands operate on the local file after the bundler has written it — which is the correct input for SRI. If you need to verify a deployed file, download it first with curl -s --compressed (to decompress transport encoding), then pipe to openssl.

Webpack: webpack-subresource-integrity

This plugin integrates with Webpack’s asset emit pipeline and patches the runtime chunk loader to include integrity values for dynamically imported chunks:

npm install --save-dev webpack-subresource-integrity html-webpack-plugin
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    // Required: enables CORS on dynamically loaded chunks
    crossOriginLoading: 'anonymous',
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      inject: true,
    }),
    new SubresourceIntegrityPlugin({
      // sha384 is recommended; you can include multiple algorithms
      hashFuncNames: ['sha384'],
      // 'always' regardless of mode; 'auto' only in production
      enabled: 'always',
    }),
  ],
  optimization: {
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
    },
  },
};

The plugin writes integrity values into the Webpack stats object. HtmlWebpackPlugin reads those stats and emits correct <script integrity="..." crossorigin="anonymous"> tags automatically. For dynamic chunks, the plugin patches __webpack_require__.l so every runtime fetch() includes the precomputed integrity string.

For Webpack output hashing configuration details independent of SRI, see the dedicated Webpack guide.

Vite: vite-plugin-sri3

Vite 5 uses Rollup 4 under the hood. The vite-plugin-sri3 package hooks into the writeBundle phase to compute hashes and rewrite the HTML output:

npm install --save-dev vite-plugin-sri3
// vite.config.js
import { defineConfig } from 'vite';
import sri from 'vite-plugin-sri3';

export default defineConfig({
  build: {
    // Ensure deterministic filenames for reliable SRI
    rollupOptions: {
      output: {
        entryFileNames: 'entry-[name]-[hash:8].js',
        chunkFileNames: 'chunks/[name]-[hash:8].js',
        assetFileNames: 'assets/[name]-[hash:8][extname]',
      },
    },
    // Write manifest so post-build scripts can also read integrity values
    manifest: true,
  },
  plugins: [
    sri({
      algorithms: ['sha384'],
      // Set to true to also cover link[rel=preload] tags
      ignoreMissingResource: false,
    }),
  ],
});

After build, dist/index.html will include:

<script
  type="module"
  src="/entry-main-a1b2c3d4.js"
  integrity="sha384-..."
  crossorigin="anonymous"
></script>
<link
  rel="stylesheet"
  href="/assets/index-b3c4d5e6.css"
  integrity="sha384-..."
  crossorigin="anonymous"
/>

Note that vite-plugin-sri3 does not patch Vite’s runtime dynamic import mechanism. If your application uses import() for route-based code splitting, the dynamically loaded chunks will not carry integrity attributes unless you configure an import map or use a separate runtime approach.

Custom Rollup generateBundle Hook

For Rollup asset optimization workflows without a higher-level framework, add a small plugin directly:

// rollup.config.js
import { createHash } from 'node:crypto';

function sriPlugin(algorithms = ['sha384']) {
  const integrityMap = new Map();

  return {
    name: 'sri',
    generateBundle(_options, bundle) {
      for (const [fileName, chunk] of Object.entries(bundle)) {
        if (chunk.type === 'chunk' || chunk.type === 'asset') {
          const content =
            chunk.type === 'chunk'
              ? Buffer.from(chunk.code, 'utf8')
              : Buffer.isBuffer(chunk.source)
                ? chunk.source
                : Buffer.from(chunk.source, 'utf8');

          const hashes = algorithms.map((algo) => {
            const digest = createHash(algo).update(content).digest('base64');
            return `${algo}-${digest}`;
          });
          integrityMap.set(fileName, hashes.join(' '));
        }
      }
    },
    writeBundle() {
      // Emit the integrity map as a JSON file for consumption by the HTML template
      this.emitFile({
        type: 'asset',
        fileName: 'integrity-manifest.json',
        source: JSON.stringify(Object.fromEntries(integrityMap), null, 2),
      });
    },
  };
}

export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    entryFileNames: '[name]-[hash:8].js',
    chunkFileNames: '[name]-[hash:8].js',
    assetFileNames: 'assets/[name]-[hash:8][extname]',
  },
  plugins: [sriPlugin(['sha384'])],
};

The integrity-manifest.json output maps each filename to its integrity string. Your HTML template (Nunjucks, EJS, Handlebars, or server-rendered) reads this file and renders integrity attributes accordingly.

Node.js Manifest Script (Post-Build)

When your build tool does not support plugin hooks, a post-build Node.js script reading the Vite or Webpack manifest achieves the same result:

// scripts/sri-manifest.mjs
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const DIST = join(__dirname, '..', 'dist');

// Vite manifest lives at dist/.vite/manifest.json
// Format: { "src/main.ts": { "file": "assets/main-a1b2c3d4.js", "css": ["assets/index-b3c4d5e6.css"] } }
const VITE_MANIFEST = join(DIST, '.vite', 'manifest.json');
const INTEGRITY_OUTPUT = join(DIST, 'sri-manifest.json');

async function sha384(filePath) {
  const bytes = await readFile(filePath);
  return 'sha384-' + createHash('sha384').update(bytes).digest('base64');
}

async function main() {
  const manifest = JSON.parse(await readFile(VITE_MANIFEST, 'utf8'));
  const result = {};

  for (const [_key, entry] of Object.entries(manifest)) {
    if (entry.file) {
      const abs = join(DIST, entry.file);
      result[entry.file] = await sha384(abs);
    }
    for (const cssFile of entry.css ?? []) {
      const abs = join(DIST, cssFile);
      result[cssFile] = await sha384(abs);
    }
  }

  await writeFile(INTEGRITY_OUTPUT, JSON.stringify(result, null, 2), 'utf8');
  console.log(`Wrote ${Object.keys(result).length} integrity entries to ${INTEGRITY_OUTPUT}`);
}

main().catch((err) => { console.error(err); process.exit(1); });

Add it to your package.json build pipeline:

{
  "scripts": {
    "build": "vite build",
    "build:sri": "vite build && node scripts/sri-manifest.mjs",
    "preview": "vite preview"
  }
}

For CI workflows, see CI/CD asset pipeline integration for how to wire this script into a GitHub Actions or GitLab CI job and fail the build if the SRI manifest is empty or stale.

Verification

After building, confirm integrity values were computed and match the files:

# Verify a single file's integrity value against the manifest
FILE="dist/assets/main.a1b2c3d4.js"
EXPECTED_INTEGRITY=$(cat dist/sri-manifest.json | python3 -c \
  "import json,sys; m=json.load(sys.stdin); print(m.get('assets/main.a1b2c3d4.js','NOT_FOUND'))")
ACTUAL_INTEGRITY="sha384-$(openssl dgst -sha384 -binary "$FILE" | openssl base64 -A)"
if [ "$EXPECTED_INTEGRITY" = "$ACTUAL_INTEGRITY" ]; then
  echo "PASS: integrity matches"
else
  echo "FAIL: expected $EXPECTED_INTEGRITY but computed $ACTUAL_INTEGRITY"
  exit 1
fi

When to Reconsider

Automated build-pipeline SRI generation becomes problematic when:

  • Assets are post-processed by the CDN — if Cloudflare Auto Minify or CloudFront edge functions modify asset bytes after your build computes hashes, the served bytes differ from the hashed bytes. Disable CDN content transforms for SRI-protected assets.
  • You use CDN-level bundling or on-the-fly concatenation — hashes must be computed on the exact bytes the browser receives. If the CDN concatenates two files into one, no build-time hash covers the concatenated output.
  • Hashes must cover assets from a third-party origin — you cannot compute hashes at build time for assets whose content you do not control. Instead, fetch and hash those assets as part of your CI pipeline and pin the hash in your template.