First experimental subresource-integrity signing

This commit is contained in:
Caleb James DeLisle 2018-06-07 15:50:46 +02:00
parent 0c8ce27f24
commit 5607094429
7 changed files with 238 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
I1M7/VtyN5OGut+vhPCtxW2sxbuyZ8vWkoORdeqG6DD4hY7HutC3iyYPJkk6G+m6vhgQPRQqMdUNIzCuFoIPAls4LCJKMkp5c0l2QmRWZFBweVdWSWNvYkc0SXJIZWozYmU0a3lhWDV3UE1ORFlnPSJd

115
gen-manifest.js Normal file
View File

@ -0,0 +1,115 @@
/* jshint esversion: 6 */
/* global Buffer */
const Fs = require('fs');
const nThen = require('nthen');
const Crypto = require('crypto');
const Nacl = require('tweetnacl');
//const SigKey = require('~/.cryptpad_signing_key.json');
const KEY_FILE = process.env.HOME + '/.cryptpad_signing_key.json';
const MANIFEST_FILE = './customize.dist/manifest.js';
const VERSION_FILE = './customize.dist/version.txt';
const nameMapper = (name) => {
if (name.startsWith('./www/')) {
return name.replace(/^\.\/www\//, '');
}
if (name.startsWith('./customize.dist/')) {
return name.replace(/^\.\/customize.dist\//, 'customize/');
}
throw new Error();
};
const PUBLIC_KEY = "MYaWgwAcOHIp3sZFGXeWsQX3u7U8PZrqIDaM2jNhXWY=";
const validate = (cb) => {
const key = Nacl.util.decodeBase64(PUBLIC_KEY);
Fs.readFile(VERSION_FILE, 'utf8', (err, ret) => {
if (err) { throw err; }
const buf = Nacl.util.decodeBase64(ret);
const data = Nacl.sign.open(buf, key);
cb(new Buffer(data).toString('utf8'));
});
};
const release = () => {
const key = Nacl.sign.keyPair.fromSeed(new Buffer(require(KEY_FILE).seed, 'hex'));
const files = [];
const fileHashes = {};
let manifestHash;
let lastVersion = 0;
nThen((w) => {
const recurse = (dir) => {
Fs.readdir(dir, w((err, ret) => {
if (err) { throw err; }
ret.forEach((_f) => {
const f = dir + '/' + _f;
Fs.stat(f, w((err, stat) => {
if (err) { throw err; }
if (stat.isDirectory()) {
//console.log('DIR ', f);
recurse(f);
} else if (/\.js$/.test(f)) {
//console.log('FILE', f);
files.push(f);
}
}));
});
}));
};
recurse('./www');
recurse('./customize.dist');
}).nThen((w) => {
let nt = nThen;
files.forEach((f) => {
nt = nt((w) => {
Fs.readFile(f, w((err, ret) => {
if (err) { throw err; }
fileHashes[f] = Crypto.createHash('sha256').update(ret).digest('base64');
}));
}).nThen;
});
nt(w());
}).nThen((w) => {
const manifest = { files: {} };
Object.keys(fileHashes).forEach((k) => {
let obj = manifest.files;
const elems = nameMapper(k).split('/');
const jsf = elems.pop();
elems.forEach((ke) => { obj = obj[ke] = obj[ke] || {}; });
obj[jsf] = fileHashes[k];
});
Fs.writeFile(MANIFEST_FILE, 'defineManifest(' + JSON.stringify(manifest) + ');', w((err) => {
if (err) { throw err; }
}));
}).nThen((w) => {
Fs.readFile(MANIFEST_FILE, w((err, ret) => {
if (err) { throw err; }
manifestHash = Crypto.createHash('sha256').update(ret).digest('base64');
}));
}).nThen((w) => {
validate(w((ver) => { lastVersion = JSON.parse(ver)[0]; }));
}).nThen((w) => {
const data = [ lastVersion + 1, manifestHash ];
const sig = Nacl.sign(new Buffer(JSON.stringify(data), 'utf8'), key.secretKey);
data.push(sig);
Fs.writeFile(VERSION_FILE, new Buffer(sig).toString('base64'), w((err) => {
if (err) { throw err; }
console.log('Saved!');
}));
});
};
const keypair = () => {
if (Fs.exists(KEY_FILE)) { throw new Error(); }
const data = JSON.stringify({ seed: Crypto.randomBytes(32).toString('hex') });
Fs.writeFile(KEY_FILE, data, (err) => {
if (err) { throw err; }
console.log('Generated');
});
//console.log(Nacl.sign.keyPair());
};
//keypair();
release();
//validate();

View File

@ -156,6 +156,32 @@ if (config.privKeyAndCertFiles) {
};
}
// TODO race
let version;
Fs.readFile('./customize.dist/version.txt', 'utf8', (err, v) => {
if (err) { throw err; }
version = v;
});
app.get('/api/config.json', function (req, res) {
const host = req.headers.host.replace(/\:[0-9]+/, '');
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({
requireConf: {
waitSeconds: 60,
urlArgs: 'ver=' + Package.version + (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): ''),
},
removeDonateButton: (config.removeDonateButton === true),
allowSubscriptions: (config.allowSubscriptions === true),
websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath,
websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' +
websocketPort + '/cryptpad_websocket',
httpUnsafeOrigin: config.httpUnsafeOrigin,
httpSafeOrigin: config.httpSafeOrigin || null,
httpSafePort: config.httpSafePort || null,
versionSig: version
}));
});
app.get('/api/config', function(req, res){
var host = req.headers.host.replace(/\:[0-9]+/, '');
res.setHeader('Content-Type', 'text/javascript');

View File

@ -1,7 +1,8 @@
// This is stage 1, it can be changed but you must bump the version of the project.
define([
'/common/requireconfig.js'
], function (RequireConfig) {
'/common/requireconfig.js',
'/api/config'
], function (RequireConfig, ApiConfig) {
require.config(RequireConfig());
// most of CryptPad breaks if you don't support isArray
@ -26,6 +27,14 @@ define([
Number.MAX_SAFE_INTEGER = 9007199254740991;
}
if (!ApiConfig.httpSafeOrigin) {
if (ApiConfig.httpSafePort) {
ApiConfig.httpSafeOrigin =
window.location.origin.replace(/\:[0-9]+$/, ':' + ApiConfig.httpSafePort);
}
ApiConfig.httpSafeOrigin = window.location.origin;
}
var failStore = function () {
console.error(new Error('wut'));
require(['jquery'], function ($) {

83
www/common/sboot.js Normal file
View File

@ -0,0 +1,83 @@
/*@flow*/
// The hash of this file is stored directly in the html
// YOU MUST NOT CHANGE THIS FILE
(function () {
var PUBLIC_KEY = "MYaWgwAcOHIp3sZFGXeWsQX3u7U8PZrqIDaM2jNhXWY=";
var NACL_HASH = "kcvFQQilxR1viAWvO4PrBTfQylz8bJTlYTpKQ6XeGtw=";
var load = function (url, cb) {
var req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) { cb(req.responseText); }
};
req.open("GET", url);
req.send();
};
var script = function (path, hash, done) {
var scr = document.createElement('script');
scr.async = true;
scr.charset = 'utf-8';
scr.type = 'text/javascript';
scr.src = path;
if (done) { scr.addEventListener('load', done, false); }
scr.setAttribute('integrity', 'sha256-' + hash);
document.getElementsByTagName('head')[0].appendChild(scr);
};
var ConfigS;
var Config;
var Nacl;
var three = function (manifest) {
delete window.defineManifest;
var requireHash = manifest.files.bower_components.requirejs['require.js'];
script(
'/bower_components/requirejs/require.js?ver=' + encodeURIComponent(requireHash),
requireHash,
function () {
var rcfg = Config.requireConf || {};
rcfg.onNodeCreated = function (node /*, config, module, path*/) {
var src = node.getAttribute('src');
var hash = manifest.files;
var path = src.replace(/\?.*$/, '').split('/');
path.shift(); // leading /
for (var i = 0; i < path.length; i++) {
var next = hash[path[i]];
if (!next) { throw new Error("no entry in manifest for " + src); }
hash = next;
}
if (typeof(hash) !== 'string') { throw new Error("no entry in manifest for " + src); }
var newSrc = src.replace(/([\?\&])ver=[^\?\&]*([\?\&])?/, function (all, begin, end) {
return begin + 'ver=' + encodeURIComponent(hash) + (end || '');
});
node.setAttribute('src', newSrc);
node.setAttribute('integrity', 'sha256-' + hash);
};
window.require.config(Config.requireConf);
window.define('/api/config', function () { return JSON.parse(ConfigS); });
window.require(['/common/boot2.js']);
}
);
};
var two = function () {
if (!(Nacl && ConfigS)) { return; }
Config = JSON.parse(ConfigS);
var key = Nacl.util.decodeBase64(PUBLIC_KEY);
var buf = Nacl.util.decodeBase64(Config.versionSig);
var opened = Nacl.sign.open(buf, key);
var openedS = Nacl.util.encodeUTF8(opened);
var data = JSON.parse(openedS);
var lastVer = Number(localStorage.CRYPTPAD_SBOOT_VERSION || -1);
if (!lastVer) {
} else if (lastVer > data[0]) {
window.alert("Cryptpad secure bootloader failed because version number is lower than last version");
throw new Error(lastVer + " > " + data[0]);
}
localStorage['CRYPTPAD_SBOOT_VERSION'] = data[0];
window.defineManifest = three;
script("/customize/manifest.js?ver=" + encodeURIComponent(data[1]), data[1]);
};
script(
'/bower_components/tweetnacl/nacl-fast.min.js?ver=' + encodeURIComponent(NACL_HASH),
NACL_HASH,
function () { Nacl = window.nacl; delete window.nacl; two(); }
);
load("/api/config.json?ver=" + (+new Date()), function (v) { ConfigS = v; two(); });
}());

View File

@ -5,7 +5,7 @@
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/common/sframe-app-outer.js" src="/common/sboot.js"></script>
<style>
html, body {
margin: 0px;