0

I'm developing a Blazor WebAssembly app with PWA enabled, and with files appsettings.json, appsettings.Development.json and appsettings.Production.json. The last one is empty because it would contain secrets to replace when production environment is deployed to a kubernetes cluster.

I'm using k8s to deploy, and a Secret resource to replace the empty appsettings.Production.json file by an encrypted file, into a nginx based container with the published blazor app inside.

Now I'm getting this issue in the browser: Failed integrity validation on resource

When the application was built using docker build in a CI pipeline, the file was an empty json file, and got a SHA computed that does not match then one computed by the build process.

My question is: How can I replace the appsettings.Production.json during deployment, much later than the build process, and don't have the integrity test failed over that file?

The file blazor.boot.json does not contain any SHA for the appsetting.Production.json file:

{
  "cacheBootResources": true,
  "config": [
    "appsettings.Development.json",
    "appsettings.json",
    "appsettings.Production.json"
  ],
  "debugBuild": false,
  "entryAssembly": "IrisTenantWeb",
  "icuDataMode": 0,
  "linkerEnabled": true,
  "resources": {
    "assembly": {
      "Azure.Core.dll": "sha256-rzNx\/GlDpiutVRPzugT82owXvTopmiixMar68xLA6L8=",
      // Bunch of .dlls,
      "System.Private.CoreLib.dll": "sha256-S7l+o9J9ivjCunMa+Ms\/JO\/kVaXLW8KTAjq1eRjY4EA="
    },
    "lazyAssembly": null,
    "pdb": null,
    "runtime": {
      "dotnet.timezones.blat": "sha256-SQvzbzBfueaAxSKIKE1khBH02NH2MJJaWDBav\/S5MSs=",
      "dotnet.wasm": "sha256-YXYNlLeMqRPFVpY2KSDhleLkNk35d9KvzzwwKAoiftc=",
      "icudt.dat": "sha256-m7NyeXyxM+CL04jr9ui1Z6pVfMWwhHusuz5qNZWpAwA=",
      "icudt_CJK.dat": "sha256-91bygK5voY9lG5wxP0\/uj7uH5xljF9u7iWnSldT1Z\/g=",
      "icudt_EFIGS.dat": "sha256-DPfeOLph83b2rdx40cKxIBcfVZ8abTWAFq+RBQMxGw0=",
      "icudt_no_CJK.dat": "sha256-oM7Z6aN9jHmCYqDMCBwFgFAYAGgsH1jLC\/Z6DYeVmmk=",
      "dotnet.5.0.5.js": "sha256-Dvb7uXD3+JPPqlsw2duS+FFNQDkFaxhIbSQWSnhODkM="
    },
    "satelliteResources": null
  }
}

But the service-worker-assets.js file DOES contains a SHA computed for it:

self.assetsManifest = {
  "assets": [
    {
      "hash": "sha256-EaNzjsIaBdpWGRyu2Elt6mv3X+48iD9gGaSN8xAm3ao=",
      "url": "appsettings.Development.json"
    },
    {
      "hash": "sha256-RIn54+RUdIs1IeshTgpWlNViz\/PZ\/1EctFaVPI9TTAA=",
      "url": "appsettings.json"
    },
    {
      "hash": "sha256-RIn54+RUdIs1IeshTgpWlNViz\/PZ\/1EctFaVPI9TTAA=",
      "url": "appsettings.Production.json"
    },
    {
      "hash": "sha256-OV+CP+ILUqNY7e7\/MGw1L5+Gi7EKCXEYNJVyBjbn44M=",
      "url": "css\/app.css"
    },
   // ...
  ],
  "version": "j39cUu6V"
};

NOTE: You can see that both appsettings.json and appsettings.Production.json have the same hash because they are both the empty json file {}. But in production the second one is having a computed hash of YM2gjmV5... and issuing the error.

I can't have different build processes for different environments, because that would not ensure using the same build from staging and production. I need to use the same docker image but replacing the file at deployment time.

isierra
  • 93
  • 6
  • Maybe you can add an initContainer that mount the `appsettings.Production.json` from the secret, takes `service-worker-assets.js` from the docker image and replaces the hash value with computed hash value of the mounted file (using any `sed`-like tool) before storing the modified `service-worker-assets.js` in an `emptyDir` volumeMount. Then in the application container, mount the `emptyDir` volumeMount and mount and use that `service-worker-assets.js` instead. – Lukman Apr 30 '21 at 15:13
  • That looks too hacky for me. I think that process could work but it is too brittle, and any change in the future could break that patch. – isierra May 23 '21 at 19:24

1 Answers1

3

I edited the wwwroot/service-worker.published.js file, which first lines are as follow:

// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations

self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));

const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];

async function onInstall(event) {
    console.info('Service worker: Install');

    // Fetch and cache all matching items from the assets manifest
    const assetsRequests = self.assetsManifest.assets
        .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
        .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
        .map(asset => new Request(asset.url, { integrity: asset.hash }));
    await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
...

I added an array of patterns, similar to offlineAssetsInclude and offlineAssetsExclude to indicate which files I want to skip integrity checks.

...
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
const integrityExclude = [ /^appsettings\.Production\.json$/ ]; // <-- new variable

Then at onInstall, instead of always returning a Request with integrity set, I skipped it for excluded patterns:

...
async function onInstall(event) {
    console.info('Service worker: Install');

    // Fetch and cache all matching items from the assets manifest
    const assetsRequests = self.assetsManifest.assets
        .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
        .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
        .map(asset => {
            // Start of new code
            const integrity =
                integrityExclude.some(pattern => pattern.test(asset.url))
                ? null
                : asset.hash;

            return !!integrity 
              ? new Request(asset.url, { integrity }) 
              : new Request(asset.url);
            // End of new code
        });
    await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
...

I'll wait for others to comment and propose other solutions, because the ideal response would set the correct SHA hash to the file, instead of ignoring it.

isierra
  • 93
  • 6
  • For me, I got the error `'Error parsing 'integrity' attribute ('null'). The hash algorithm must be one of 'sha256', 'sha384', or 'sha512', followed by a '-' character.'` But the general idea is great! Exactly what I've been looking for. I rewrote your snippet to return `new Request(asset.url)` instead of null, and it seems to work. – PuerNoctis Jun 15 '21 at 07:36
  • Thanks @PuerNoctis. I just edited the answer to reflect that issue. I forgot to update the answer when I detected this. I assume you can also use `undefined` instead of `null` for `integrity`, but I didn't tested it. – isierra Jun 17 '21 at 14:24