Fix Blazor WebAssembly PWA integrity checks
Niels Swimberghe - - .NET
Follow me on Twitter, buy me a coffee
I recently released a how-to article and video on deploying Blazor WASM to GitHub Pages. In this content I demonstrated how to deploy Blazor WASM to GitHub pages manually and automatically using GitHub Actions, but I did not have the PWA feature enabled. When you create a Blazor WASM application, you can pass a --pwa
flag to include PWA support.
It turns out that the PWA feature breaks in those how-to's. The service worker requests all the files with integrity checking, and in those how-to's the base-tag of the index.html file is modified. This modification causes the integrity check to fail because the pre-calculated hash stored in service-worker-assets.js for index.html mismatches. The result was the following error:
Service worker: Install Failed to find a valid digest in the 'integrity' attribute for resource 'http://YOURURL/index.html' with computed SHA-256 integrity 'sYKnuzlqE606K8G3ejaHYO2arpP3AQOjtIDxiCzAKyA='. The resource has been blocked. Unknown error occurred while trying to verify integrity. service-worker.js:1 Uncaught (in promise) TypeError: Failed to fetch
This issue will surface when any modification is made to the files listed in the service-worker-assets.js file. The service-worker-assets.js file is generated during publish and any modification made to the listed files after publish will cause the integrity check to fail.
Solution #
Microsoft's documentation has some guidance on how to detect and troubleshoot this issue. You have the option to disable the integrity checking altogether, but you will lose the safety guarantees offered by integrity checking.
Alternatively, you can calculate the new hash for all the modified files and update them in the service-worker-assets.js file. You can calculate the hash with with these PowerShell commands:
$Signature = Get-FileHash -Path "path/to/your/file" -Algorithm SHA256 $SignatureBytes = [byte[]] -split ($Signature.Hash -replace '..', '0x$& ') $SignatureBase64 = [System.Convert]::ToBase64String($SignatureBytes) $NewHash = "sha256-$SignatureBase64" Write-Host $NewHash
Alternatively, you can use openssl to generate the hash in bash:
echo "sha256-$(openssl dgst -sha256 -binary \\path\\to\\your\\file | openssl base64 -A)"
Manually going over each file and updating the hash in the service-worker-assets.js file can be painful, so here's a PowerShell script and a Bash script that will iterate over every file listed in service-worker-assets.js.
If the hash is different, the hash is automatically updated in service-worker-assets.js:
PowerShell:
# make sure you're in the wwwroot folder of the published application $JsFileContent = Get-Content -Path service-worker-assets.js -Raw # remove JavaScript from contents so it can be interpreted as JSON $Json = $JsFileContent.Replace("self.assetsManifest = ", "").Replace(";", "") | ConvertFrom-Json # grab the assets JSON array $Assets = $Json.assets foreach ($Asset in $Assets) { $OldHash = $Asset.hash $Path = $Asset.url $Signature = Get-FileHash -Path $Path -Algorithm SHA256 $SignatureBytes = [byte[]] -split ($Signature.Hash -replace '..', '0x$& ') $SignatureBase64 = [System.Convert]::ToBase64String($SignatureBytes) $NewHash = "sha256-$SignatureBase64" If ($OldHash -ne $NewHash) { Write-Host "Updating hash for $Path from $OldHash to $NewHash" # slashes are escaped in the js-file, but PowerShell unescapes them automatically, # we need to re-escape them $OldHash = $OldHash.Replace("/", "\/") $NewHash = $NewHash.Replace("/", "\/") $JsFileContent = $JsFileContent.Replace("""$OldHash""", """$NewHash""") } } Set-Content -Path service-worker-assets.js -Value $JsFileContent -NoNewline
Bash:
#!/bin/bash # make sure you're in the wwwroot folder of the published application jsFile=$(<service-worker-assets.js) # remove JavaScript from contents so it can be interpreted as JSON json=$(echo "$jsFile" | sed "s/self.assetsManifest = //g" | sed "s/;//g") # grab the assets JSON array assets=$(echo "$json" | jq '.assets[]' -c) for asset in $assets do oldHash=$(echo "$asset" | jq '.hash') #remove leading and trailing quotes oldHash="${oldHash:1:-1}" path=$(echo "$asset" | jq '.url') #remove leading and trailing quotes path="${path:1:-1}" newHash="sha256-$(openssl dgst -sha256 -binary $path | openssl base64 -A)" if [ $oldHash != $newHash ]; then # escape slashes for json oldHash=$(echo "$oldHash" | sed 's;/;\\/;g') newHash=$(echo "$newHash" | sed 's;/;\\/;g') echo "Updating hash for $path from $oldHash to $newHash" # escape slashes second time for sed oldHash=$(echo "$oldHash" | sed 's;/;\\/;g') jsFile=$(echo -n "$jsFile" | sed "s;$oldHash;$newHash;g") fi done echo -n "$jsFile" > service-worker-assets.js
The Current Working Directory (CWD) has to be at the wwwroot folder of the published application. You can now integrate these scripts into your build process or continuous integration pipeline, as done in this GitHub Actions workflow:
name: Deploy to GitHub Pages # Run workflow on every push to the master branch on: push: branches: [ pwa ] jobs: deploy-to-github-pages: # use ubuntu-latest image to run steps on runs-on: ubuntu-latest steps: # uses GitHub's checkout action to checkout code form the master branch - uses: actions/checkout@v2 # sets up .NET Core SDK 5.0.101 - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: dotnet-version: 5.0.101 # publishes Blazor project to the release-folder - name: Publish .NET Core Project run: dotnet publish BlazorGitHubPagesDemo.csproj -c Release -o release --nologo # changes the base-tag in index.html from '/' to 'BlazorGitHubPagesDemo' to match GitHub Pages repository subdirectory - name: Change base-tag in index.html from / to BlazorGitHubPagesDemo run: sed -i 's/<base href="\/" \/>/<base href="\/BlazorGitHubPagesDemo\/" \/>/g' release/wwwroot/index.html # changes the base-tag in index.html from '/' to 'BlazorGitHubPagesDemo' to match GitHub Pages repository subdirectory - name: Fix service-worker-assets.js hashes working-directory: release/wwwroot run: | jsFile=$(<service-worker-assets.js) # remove JavaScript from contents so it can be interpreted as JSON json=$(echo "$jsFile" | sed "s/self.assetsManifest = //g" | sed "s/;//g") # grab the assets JSON array assets=$(echo "$json" | jq '.assets[]' -c) for asset in $assets do oldHash=$(echo "$asset" | jq '.hash') #remove leading and trailing quotes oldHash="${oldHash:1:-1}" path=$(echo "$asset" | jq '.url') #remove leading and trailing quotes path="${path:1:-1}" newHash="sha256-$(openssl dgst -sha256 -binary $path | openssl base64 -A)" if [ $oldHash != $newHash ]; then # escape slashes for json oldHash=$(echo "$oldHash" | sed 's;/;\\/;g') newHash=$(echo "$newHash" | sed 's;/;\\/;g') echo "Updating hash for $path from $oldHash to $newHash" # escape slashes second time for sed oldHash=$(echo "$oldHash" | sed 's;/;\\/;g') jsFile=$(echo -n "$jsFile" | sed "s;$oldHash;$newHash;g") fi done echo -n "$jsFile" > service-worker-assets.js # copy index.html to 404.html to serve the same file when a file is not found - name: copy index.html to 404.html run: cp release/wwwroot/index.html release/wwwroot/404.html # add .nojekyll file to tell GitHub pages to not treat this as a Jekyll project. (Allow files and folders starting with an underscore) - name: Add .nojekyll file run: touch release/wwwroot/.nojekyll - name: Commit wwwroot to GitHub Pages uses: JamesIves/github-pages-deploy-action@3.7.1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages FOLDER: release/wwwroot
Hopefully this helps and saves you some valuable time, cheers!