Swimburger

Fix Blazor WebAssembly PWA integrity checks

Niels Swimberghe

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!

Related Posts

Related Posts