Generating SRI Hashes for CDN Scripts

This guide is part of the Subresource Integrity (SRI) reference. Subresource Integrity pins a cryptographic hash to every <script> and <link rel="stylesheet"> you load from a third-party origin. When the browser fetches the resource it recomputes the digest over the exact response bytes and refuses to execute or apply anything whose digest does not match the integrity attribute. A compromised CDN, a poisoned cache, or a tampering proxy therefore cannot silently swap your jquery.min.js for an exfiltration payload — the browser blocks the load instead. This page gives you the exact commands to produce those hashes, the build-tool automation that keeps them current, and the crossorigin and fallback rules that make SRI actually enforce rather than silently fail open.

Configuration Syntax & Exact Values

The integrity attribute is a space-separated list of one or more hash expressions. Each expression is a hash algorithm token, a hyphen, and the base64-encoded raw digest of the resource body. The algorithm token is one of sha256, sha384, or sha512. sha384 is the working default: it is longer than sha256 (blunting theoretical collision shortcuts) without the size overhead of sha512.

<script src="https://cdn.example.com/lib/chart@4.4.1/dist/chart.umd.min.js"
        integrity="sha384-Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkFTfd0Kq8i0i9J1cQ3n9pXm9m1k"
        crossorigin="anonymous"></script>

Three parts of that tag are load-bearing and each fails a different way if wrong:

You may list several expressions separated by spaces. The browser evaluates them by strongest algorithm present — it picks the highest-strength algorithm it supports, computes that one digest, and requires it to match one of the listed values of that algorithm. Listing multiple algorithms is a migration aid, not a “match any of these files” mechanism.

<link rel="stylesheet" href="https://cdn.example.com/ui/2.9/theme.css"
      integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQ ... sha512-Rk9uP0uY+... "
      crossorigin="anonymous">
Anatomy of an integrity attribute value A hash expression broken into its algorithm token, separator, and base64-encoded digest, with the crossorigin requirement called out. sha384 - Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkF... algorithm token sha256 | sha384 | sha512 separator hyphen base64 of raw digest binary SHA output, NOT hex cross-origin resource also needs crossorigin="anonymous" without it the fetch is opaque, unreadable, and the load is blocked
The digest is the base64 encoding of the raw binary SHA output — never the hex string that hashing tools print by default.

The single most common mistake is base64-encoding the hex digest instead of the raw binary digest. sha384sum and shasum print hex by default; you must feed the binary form into base64. The commands below do that correctly.

Server-Side Configuration

“Server-side” here means the machine or pipeline where you generate the hash — your workstation or CI runner — plus the tooling that keeps it synchronized. Every command below reads the exact bytes the CDN serves and emits a paste-ready hash expression.

openssl (portable, any Unix or Git Bash)

openssl is present on virtually every Linux and macOS host. The -binary flag is the critical one: it emits the raw digest bytes so the piped openssl base64 encodes the correct value. base64 -A keeps the output on a single line with no wrapping.

# Hash a local file
openssl dgst -sha384 -binary chart.umd.min.js | openssl base64 -A

# Equivalent, streaming the file in
cat chart.umd.min.js | openssl dgst -sha384 -binary | openssl base64 -A

# Prepend the algorithm token to get a paste-ready expression
echo "sha384-$(openssl dgst -sha384 -binary chart.umd.min.js | openssl base64 -A)"

To hash the file the CDN actually serves — the only bytes the browser will ever validate against — pull it over the wire first. Never hash your local node_modules copy and assume the CDN copy is byte-identical; minifier versions and line endings differ.

curl -sSL https://cdn.example.com/lib/chart@4.4.1/dist/chart.umd.min.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

shasum / sha384sum (coreutils)

shasum -a 384 and the GNU sha384sum binary print hex, so decode the hex to binary before base64. xxd -r -p performs the hex-to-binary step.

# shasum prints "<hex>  <filename>"; take field 1, unhex, base64
shasum -a 384 chart.umd.min.js | cut -d' ' -f1 | xxd -r -p | base64 -w0

# GNU coreutils equivalent
sha384sum chart.umd.min.js | cut -d' ' -f1 | xxd -r -p | base64 -w0

Prefer the openssl form when it is available — it avoids the hex round-trip entirely and is harder to get wrong.

Vite (build-time automation)

For bundled assets that change on every deploy, generate integrity at build time rather than by hand. The vite-plugin-sri family walks the emitted HTML and injects integrity plus crossorigin on every <script> and stylesheet <link>.

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

export default defineConfig({
  plugins: [
    sri({ algorithm: 'sha384' }),
  ],
});

webpack (build-time automation)

webpack-subresource-integrity computes hashes over the final emitted chunks. It requires output.crossOriginLoading to be set so the injected <script> tags carry a crossorigin attribute — without it dynamically loaded chunks fail SRI in the same way a hand-written tag would.

// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');

module.exports = {
  output: {
    crossOriginLoading: 'anonymous', // REQUIRED: emits crossorigin on injected tags
  },
  plugins: [
    new SubresourceIntegrityPlugin({
      hashFuncNames: ['sha384'],
      enabled: process.env.NODE_ENV === 'production',
    }),
  ],
};
Pipeline from response bytes to integrity attribute A left-to-right data flow showing the CDN response body passing through a SHA function, base64 encoding, and token prefixing to produce the integrity value. CDN response exact served bytes SHA-384 -binary, raw digest base64 -A single line, no wrap sha384-… integrity value skip -binary and you hash the HEX string produces a valid-looking but wrong digest
Each stage reads the previous stage's raw output; dropping the binary flag silently poisons the whole chain.

Diagnostic & Verification Steps

Verify a hash before you deploy it, then confirm the browser enforces it after.

Recompute and compare against the tag you are about to ship. Identical output means the pinned value matches the live CDN bytes:

$ curl -sSL https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js \
    | openssl dgst -sha384 -binary | openssl base64 -A
Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkFTfd0Kq8i0i9J1cQ3n9pXm9m1k

Confirm the CDN sends the CORS header SRI needs. access-control-allow-origin: * (or your origin) must be present or crossorigin="anonymous" cannot succeed:

$ curl -sSI https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js \
    | grep -i access-control-allow-origin
access-control-allow-origin: *

Confirm the browser enforces the pin. Load the page, then deliberately corrupt one character of the integrity value and reload — DevTools Console prints a precise mismatch error and the resource is blocked:

Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.example.com/lib/chart.umd.min.js' with computed SHA-384 integrity
'Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkFTfd0Kq8i0i9J1cQ3n9pXm9m1k'.
The resource has been blocked.

That error is the single most useful artifact: the browser prints the digest it computed, so you can copy the value straight from the console into your tag when a CDN version bump changes the bytes.

Browser SRI validation sequence A vertical sequence showing the browser fetching a resource, checking CORS readability, recomputing the digest, comparing it, and either executing or blocking. Parse tag: src + integrity Fetch resource (CORS) Response readable? needs allow-origin match Recompute strongest hash Digest == integrity? Execute / apply Block resource console mismatch error match mismatch / opaque
An unreadable (opaque) response fails at the CORS gate before hashing ever happens — the same block outcome as a digest mismatch.

Edge Cases, Security Implications & Safe Rollback

Three traps account for nearly every broken SRI deployment:

Rollback is mechanical because SRI lives entirely in the markup. To disable enforcement for one resource in an incident, remove that tag’s integrity attribute (and crossorigin if it is otherwise unused) and redeploy — the browser then loads the resource without validation. Because the pin is per-tag, you roll back exactly one script without touching any other. Keep the previous known-good hash in version control so re-enabling is a one-line revert once the CDN mismatch is understood.

Conclusion

Roll SRI out in the same staged way as any enforcement header. In staging, add integrity to one non-critical CDN asset and confirm it loads, then corrupt the hash to confirm the block fires and the console error appears. Promote to production behind a short cache lifetime so a bad pin can be flushed quickly, watching your error telemetry for integrity mismatches the way you would watch a Content-Security-Policy (CSP) report stream. Once a version-pinned asset validates cleanly, extend the same generated-hash workflow to every third-party script and stylesheet and let your build tool keep the values current.

Frequently Asked Questions

Why is my integrity value rejected even though the file looks correct? The most common cause is base64-encoding the hex digest instead of the raw binary digest. Tools like sha384sum and shasum -a 384 print hex; you must run it through xxd -r -p before base64, or use openssl dgst -sha384 -binary which emits raw bytes directly. A hex-then-base64 value is the wrong length and always fails.

Do I need crossorigin="anonymous" on same-origin scripts? No. SRI on a same-origin resource works without crossorigin because the browser can already read the response bytes. The attribute is required only for cross-origin resources such as CDN assets, where SRI validation depends on a CORS-readable response.

Which algorithm should I use — sha256, sha384, or sha512? Use sha384 as the default. All three are valid SRI algorithms and the browser always validates against the strongest one present. sha384 gives more collision margin than sha256 without the larger attribute size of sha512, and it is the value emitted by most CDN “copy integrity” buttons.

What happens if I list two algorithms in one integrity attribute? The browser picks the strongest algorithm it supports from the list, computes that single digest, and requires it to match one of the listed values for that algorithm. It is a migration and prioritization mechanism, not a way to whitelist two different files under one tag.

Does SRI protect against a CDN going down or returning a 404? No. SRI only verifies the integrity of bytes that are successfully fetched; it does nothing for availability. A network failure or 404 is a normal load error. If you need resilience, add a fallback loader that pulls a same-origin copy — and give that fallback its own integrity so it is verified too.