LEARNING OBJECTIVES ⌵
- Generate cryptographic SRI hashes using CLI tools (
openssl,shasum). - Programmatically compute SRI metadata using Node.js
crypto. - Automate SRI hash injection into production builds via Webpack and Vite plugins.
- Prevent hash mismatch bugs caused by line-ending conversions (CRLF vs. LF) and post-build transformations.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a pharmaceutical company manufacturing tamper-evident medicine bottles on an automated assembly line.
Before a bottle leaves the factory floor, a robotic scanner analyzes its chemical composition, computes a unique cryptographic barcode, prints that barcode on the shipping manifest (the HTML document), and seals the bottle for transport.
+---------------------------------------------------------------------------------------+
| THE AUTOMATED HASH GENERATION PIPELINE |
+---------------------------------------------------------------------------------------+
| |
| Source Code Bundler (Vite/Webpack) Production Assets |
| +---------------+ +------------------------+ +------------------+ |
| | src/app.js | ------> | Minify & Tree Shake | -------> | dist/app.a1b2.js | |
| +---------------+ +------------------------+ +------------------+ |
| | | |
| v v |
| [ Compute SHA-384 Digest ] ---------> [ Base64 Digest ] |
| | |
| v |
| [ Inject into HTML ] |
| | |
| v |
| +------------------------+ |
| | dist/index.html | |
| | <script src="app.js" | |
| | integrity="sha384-.."> | |
| +------------------------+ |
+---------------------------------------------------------------------------------------+
Manually calculating hashes with a calculator for every code change would be impossible in modern continuous deployment. Every time you fix a single typo or update a dependency, the file's binary bytes change, generating an entirely new hash.
Therefore, production engineering requires both command-line fluency for one-off third-party vendor assets and automated build plugins for first-party bundles.
Technical Deep Dive & Specifications
1. Generating Hashes via Command Line
Method A: Using OpenSSL (Universal Standard)
The OpenSSL command computes the binary digest and pipes the raw bytes directly into the Base64 encoder:
# General Syntax for SHA-384:
openssl dgst -sha384 -binary bundle.min.js | openssl base64 -A
# Output Example:
# 4Lz5vI3Yn5FzM8P1Q2R3S4T5U6V7W8X9Y0Z1A2B3C4D5E6F7G8H9I0J1K2L3M4N==
To format it directly as an HTML-ready SRI string:
echo "sha384-$(openssl dgst -sha384 -binary bundle.min.js | openssl base64 -A)"
⚠️ Critical Flag
-binary: Without the-binaryflag,openssl dgstoutputs a hex-encoded ASCII string. Base64-encoding a hex string will produce an invalid SRI hash. Always pass-binary. The-Aflag inopenssl base64prevents newline wrapping.
Method B: Using shasum & xxd (macOS / Linux)
shasum -b -a 384 bundle.min.js | awk '{ print $1 }' | xxd -r -p | base64
2. Programmatic Generation with Node.js crypto
In Node.js scripts, tools, and build hooks, use the native node:crypto module:
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
/**
* Computes the W3C SRI string for a given file buffer.
* @param {Buffer|string} content - Raw file bytes or string
* @param {'sha256'|'sha384'|'sha512'} algorithm - Hash algorithm
* @returns {string} SRI metadata string formatted as "algorithm-base64"
*/
export function generateSRI(content, algorithm = 'sha384') {
const hash = createHash(algorithm)
.update(content)
.digest('base64');
return `${algorithm}-${hash}`;
}
// Usage:
const fileBuffer = readFileSync('./dist/assets/vendor.min.js');
console.log(generateSRI(fileBuffer, 'sha384'));
// Output: sha384-m6t5i17z/aP29Z19F4sN+eA4zR8nC9lP7qY1vX6mZ8bC2xD3eE4fG5hH6iI7jJ8k=
3. Automated Bundler Integrations
Webpack: webpack-subresource-integrity
In Webpack projects, the webpack-subresource-integrity plugin hooks into Webpack's asset compilation pipeline to calculate hashes for all output chunks and inject integrity and crossorigin attributes into HtmlWebpackPlugin.
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
publicPath: 'https://cdn.mycompany.com/assets/',
crossOriginLoading: 'anonymous', // Mandatory for Webpack dynamic chunk loading with SRI
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha384'],
enabled: process.env.NODE_ENV === 'production',
}),
],
};
Vite: vite-plugin-sri
In Vite applications, use community or custom Rollup plugins during the build lifecycle:
// vite.config.ts
import { defineConfig } from 'vite';
import sri from '@stewilond/vite-plugin-sri'; // or custom build hook
export default defineConfig({
plugins: [
sri({
algorithms: ['sha384'],
}),
],
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
});
💻 Interactive Code Playground
Starter Code: Custom Node.js SRI Build Hook
Line-by-Line Code Breakdown
- Lines 8–13 (
mockBundleJs): Represents the minified JavaScript asset produced by your build tool. - Lines 16–19 (
createHash('sha384')...): Generates the SHA-384 binary hash of the raw string content and converts it directly into a standard Base64 string. - Lines 21–22 (
sriAttributeValue): Formats the string with the requiredsha384-prefix. - Lines 44–45 (
htmlTemplate.replace(...)): Injects the complete<script>tag containing bothintegrityandcrossorigin="anonymous"into the production HTML distribution directory.
Expected Browser Render Output
// scripts/sri-generator.mjs
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
// 1. Mock asset content to simulate a production build
const mockBundleJs = `
(function() {
console.log("Enterprise Core v2.4.0 Initialized");
window.__SECURITY_VERIFIED__ = true;
})();
`;
// 2. Compute SHA-384 digest
const sha384Digest = createHash('sha384')
.update(mockBundleJs, 'utf8')
.digest('base64');
const sriAttributeValue = `sha384-${sha384Digest}`;
console.log('Calculated SRI:', sriAttributeValue);
// 3. Simulated HTML template
const htmlTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Automated SRI Build</title>
<!-- SCRIPT_INJECTION_POINT -->
</head>
<body>
<h1>SRI CI/CD Pipeline Verification</h1>
<p id="status">Verifying runtime execution...</p>
<script>
if (window.__SECURITY_VERIFIED__) {
document.getElementById('status').textContent = '✅ Script integrity verified!';
document.getElementById('status').style.color = '#059669';
}
</script>
</body>
</html>`;
// 4. Inject script tag with SRI into HTML
const scriptTag = `<script src="dist/app.bundle.js" integrity="${sriAttributeValue}" crossorigin="anonymous"></script>`;
const finalHtml = htmlTemplate.replace('<!-- SCRIPT_INJECTION_POINT -->', scriptTag);
console.log('\n--- Generated Production HTML ---');
console.log(finalHtml);SRI CI/CD Pipeline Verification
✅ Script integrity verified!🏋️ Hands-On Exercise
🎯 The Challenge: Build an SRI CLI Verification Tool in Node.js
Instructions:
- Write a Node.js utility function
generateHtmlScriptTag(url, fileContent, algorithm)that:- Accepts an asset URL, the raw file content, and an optional algorithm (defaulting to
'sha384'). - Calculates the cryptographic hash using Node.js
crypto. - Returns a complete, valid HTML
<script>tag string formatted withsrc,integrity, andcrossorigin="anonymous".
- Accepts an asset URL, the raw file content, and an optional algorithm (defaulting to
- Ensure that empty or invalid contents throw an informative Error.
- Test your function with a sample CSS stylesheet string and ensure it generates an equivalent
<link rel="stylesheet">tag.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- CRLF vs. LF Line-Ending Mismatch: On Windows systems, Git or editors may convert Line Feed (
\n) characters to Carriage Return + Line Feed (\r\n). Because cryptographic hashes operate on raw binary bytes, even a single\rcharacter change results in a completely different hash, breaking SRI in production. Configure.gitattributesto enforce* text eol=lf. - Hashing Unminified Code in Development: If you generate an SRI hash from unminified development code, but your production CDN serves minified or gzipped/brotli code, the SRI check will fail if the server alters the decompressed bytes. Note: Gzip/Brotli transfer encoding does NOT alter the hash because the browser hashes the decompressed payload bytes.
- Missing
output.crossOriginLoadingin Webpack: When Webpack code-splits dynamicimport()chunks, Webpack generates script tags on the fly. Ifoutput.crossOriginLoading: 'anonymous'is missing, Webpack generates dynamic script tags without CORS, causing dynamic chunks to fail SRI verification.
💡 Pro Tips
- Transfer Encoding Transparency: HTTP compression (like
gzip,brotli, orzstd) is transparent to SRI. The browser applies the hash check to the decoded response body payload, not the compressed wire bytes. - Automated CI/CD Parity Testing: In your CI/CD deployment pipeline, run a post-deployment verification job that fetches your production HTML, extracts all
integrityattributes, fetches the corresponding live CDN files, and asserts that calculated hashes match live values before switching production traffic.
📌 Key Takeaways
- Generate SRI hashes via OpenSSL CLI using
openssl dgst -sha384 -binary file.js | openssl base64 -A. - The
-binaryflag is required in OpenSSL to avoid base64-encoding a hex string. - In Node.js, compute hashes with
crypto.createHash('sha384').update(buffer).digest('base64'). - Automated build plugins (such as
webpack-subresource-integrityandvite-plugin-sri) inject hashes automatically into HTML templates during production builds. - SRI operates on the decompressed resource bytes, meaning
gzipandbrotliHTTP transfer encodings do not break verification. - --