Bridge: Tune the TLS related settings and code (#153)

This commit is contained in:
Michał Leszczyński
2023-04-11 01:55:29 +02:00
committed by GitHub
parent 4e2b7d04e9
commit 97f4718ca2
8 changed files with 174 additions and 79 deletions

View File

@@ -12,12 +12,12 @@
This is the example web application of HaLo Bridge Server.
Please connect your PC/SC reader and tap the compatible HaLo tag.
</p>
<pre id="log">Connecting to the server...</pre>
<pre id="log" style="word-break: break-all; white-space: pre-wrap;">Connecting to the server...</pre>
</div>
<script src="/assets/static/libhalo.js"></script>
<script>
const forceUseTLS = {% if forceUseTLS %}true{% else %}false{% endif %};
const displayTLSWarn = {% if displayTLSWarn %}true{% else %}false{% endif %};
let wsp = null;
const wsPort = {{ wsPort }};
const wssPort = {{ wssPort }};
@@ -26,16 +26,6 @@
document.getElementById('log').innerText += '\n' + data;
}
let wsAddress;
if (window.location.protocol === 'https:' || forceUseTLS) {
wsAddress = 'wss://halo-bridge.local:' + wssPort;
} else {
wsAddress = 'ws://127.0.0.1:' + wsPort;
}
let wsp = createWs(wsAddress);
async function execHaloCommand(command) {
log('Executing command: ' + JSON.stringify(command));
let res = await wsp.sendRequest(command);
@@ -69,48 +59,59 @@
});
}
wsp.onUnpackedMessage.addListener(async ev => {
if (ev.event !== "exec_success" && ev.event !== "exec_exception") {
switch (ev.event) {
case 'ws_connected':
log('Connected to the server.');
break;
case 'reader_added':
log('Reader connected: ' + ev.data.reader_name);
break;
case 'reader_removed':
log('Reader disconnected: ' + ev.data.reader_name);
break;
case 'handle_added':
log('Detected tag: ' + ev.data.reader_name + ' (handle: ' + ev.data.handle + ')');
break;
case 'handle_removed':
log('Removed tag: ' + ev.data.reader_name + ' (handle: ' + ev.data.handle + ')');
break;
async function run() {
let wsAddress = await haloFindBridge({
wsPort: wsPort,
wssPort: wssPort
});
wsp = haloCreateWs(wsAddress);
wsp.onUnpackedMessage.addListener(async ev => {
if (ev.event !== "exec_success" && ev.event !== "exec_exception") {
switch (ev.event) {
case 'ws_connected':
log('Connected to the server.');
break;
case 'reader_added':
log('Reader connected: ' + ev.data.reader_name);
break;
case 'reader_removed':
log('Reader disconnected: ' + ev.data.reader_name);
break;
case 'handle_added':
log('Detected tag: ' + ev.data.reader_name + ' (handle: ' + ev.data.handle + ')');
break;
case 'handle_removed':
log('Removed tag: ' + ev.data.reader_name + ' (handle: ' + ev.data.handle + ')');
break;
}
}
}
if (ev.event === "handle_added") {
await processTag(ev.data.handle);
}
});
if (ev.event === "handle_added") {
await processTag(ev.data.handle);
}
});
wsp.onClose.addListener(event => {
if (event.code === 1006 && displayTLSWarn) {
log('IMPORTANT: You might be missing a local certificate, which is required for halo-bridge on Safari. ' +
'If the halo-bridge doesn\'t work at all, please try to reinstall the entire toolset using the ' +
'release PKG file. When the installer will ask you about certificate generation, ' +
'please choose "Generate certificate" option.');
}
wsp.onClose.addListener(event => {
if (event.code === 1006) {
log('IMPORTANT: You might be missing a local certificate, which is required for halo-bridge on Safari. ' +
'If the halo-bridge doesn\'t work at all, please try to reinstall the entire toolset using the ' +
'release PKG file. When the installer will ask you about certificate generation, ' +
'please choose "Generate certificate" option.');
}
if (event.code === 4001) {
log('Connection closed, new client has connected.');
} else {
log('Connection closed: ' + event.code);
}
});
if (event.code === 4001) {
log('Connection closed, new client has connected.');
} else {
log('Connection closed: ' + event.code);
}
});
wsp.open();
wsp.open();
}
run();
</script>
</body>
</html>

View File

@@ -149,16 +149,11 @@ function runHalo(entryMode, args) {
if (entryMode === "server") {
console.log('Launching Web Socket Server...');
let serverInfo = wsCreateServer(
args, () => Object.keys(nfc.readers).map(r => nfc.readers[r].name));
wsCreateServer(args, () => Object.keys(nfc.readers).map(r => nfc.readers[r].name));
console.log('Web Socket Server is listening...');
if (!args.nonInteractive) {
if (serverInfo.hasTLS) {
open('https://halo-bridge.local:' + args.listenPortTLS);
} else {
open('http://127.0.0.1:' + args.listenPort);
}
open('http://127.0.0.1:' + args.listenPort);
}
} else {
stopPCSCTimeout = setTimeout(stopPCSC, 4000, "timeout", args.output);

View File

@@ -14,23 +14,24 @@ mkdir -p /usr/local/etc/halo-bridge
# remove stale files
rm -f /usr/local/etc/halo-bridge/private_key.pem
rm -f /usr/local/etc/halo-bridge/server.csr
rm -f /usr/local/etc/halo-bridge/server.csr
rm -f /usr/local/etc/halo-bridge/server.crt
# ask user whether he wants to generate a certificate for halo-bridge.local
if osascript -e 'display dialog "In order for halo-bridge to work correctly, we will need to generate a self-signed certificate for the '\''halo-bridge.local'\'' domain and mark it as trusted in the system.\n\nYou can skip that step if you don'\''t need to use halo-bridge tool." buttons {"Skip", "Generate certificate"} default button "Generate certificate" cancel button "Skip" with icon caution with title "HaLo Tools Installer"'
then
# generate new local certificate
openssl genrsa -out /usr/local/etc/halo-bridge/private_key.pem 2048
openssl req -new -sha256 -key /usr/local/etc/halo-bridge/private_key.pem -out /usr/local/etc/halo-bridge/server.csr -subj '/CN=halo-bridge.local/'
openssl req -x509 -sha256 -days 3650 -extensions SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName='DNS:halo-bridge.local'")) -key /usr/local/etc/halo-bridge/private_key.pem -in /usr/local/etc/halo-bridge/server.csr -out /usr/local/etc/halo-bridge/server.pem
openssl req -new -sha256 -key /usr/local/etc/halo-bridge/private_key.pem -out /usr/local/etc/halo-bridge/server.csr -subj '/CN=halo-tools (Local Certificate)/'
openssl req -x509 -sha256 -days 3650 -extensions HALO -config <(printf "[HALO]\nsubjectAltName='DNS:halo-bridge.local,DNS:localhost,IP:127.0.0.1,IP:::1'\nbasicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=critical,serverAuth") -key /usr/local/etc/halo-bridge/private_key.pem -in /usr/local/etc/halo-bridge/server.csr -out /usr/local/etc/halo-bridge/server.crt
# add certificate to the trust list
security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/etc/halo-bridge/server.pem
security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/etc/halo-bridge/server.crt
fi
# add halo-bridge.local domain to /etc/hosts if it doesn't exist yet
if ! grep -q "halo-bridge.local" /etc/hosts
then
echo "" >> /etc/hosts
echo "127.0.0.1 halo-bridge.local" >> /etc/hosts
fi

View File

@@ -9,6 +9,7 @@ const https = require("https");
const fs = require("fs");
const path = require("path");
const os = require("os");
const util = require("util");
let wss = null;
@@ -51,7 +52,7 @@ async function validateCSRFToken(token) {
}
function sendToCurrentWs(ws, data) {
console.log('send', data);
console.log('send', util.inspect(data, {showHidden: false, depth: null, colors: true}));
if (currentWsClient !== null && (ws === null || currentWsClient === ws)) {
currentWsClient.send(JSON.stringify(data));
@@ -125,10 +126,10 @@ function readTLSData() {
if (process.platform === "win32") {
privateKeyPath = path.join(os.homedir(), ".halo-bridge\\private_key.pem");
certificatePath = path.join(os.homedir(), ".halo-bridge\\server.pem");
certificatePath = path.join(os.homedir(), ".halo-bridge\\server.crt");
} else {
privateKeyPath = '/usr/local/etc/halo-bridge/private_key.pem';
certificatePath = '/usr/local/etc/halo-bridge/server.pem';
certificatePath = '/usr/local/etc/halo-bridge/server.crt';
}
if (fs.existsSync(privateKeyPath) && fs.existsSync(certificatePath)) {
@@ -148,7 +149,6 @@ function wsCreateServer(args, getReaderNames) {
const app = express();
const server = app.listen(args.listenPort, args.listenHost);
let forceUseTLS = (process.platform === "darwin");
let displayTLSWarn = (process.platform === "darwin");
if (tlsData) {
@@ -172,13 +172,18 @@ function wsCreateServer(args, getReaderNames) {
app.get('/', (req, res) => {
res.render('ws_client.html', {
forceUseTLS: forceUseTLS,
displayTLSWarn: displayTLSWarn,
wsPort: args.listenPort,
wssPort: args.listenPortTLS
});
});
app.get('/health', (req, res) => {
res.type('text/plain')
.set('Access-Control-Allow-Origin', req.headers.origin)
.set('Access-Control-Allow-Methods', 'GET')
.send('OK');
});
app.get('/consent', async (req, res) => {
let url = new URL(req.query.website);
let csrfToken = await makeCSRFToken();
@@ -229,6 +234,13 @@ function wsCreateServer(args, getReaderNames) {
}
wss.on('connection', (ws, req) => {
let parts = req.url.split('?');
if (parts.length === 2 && parts[1] === "ping=1") {
ws.close(4090, "Pong.");
return;
}
let permitted = false;
let originHostname = new URL(req.headers.origin).hostname;
@@ -267,7 +279,7 @@ function wsCreateServer(args, getReaderNames) {
}
let packet = JSON.parse(data);
console.log('recv', packet);
console.log('recv', util.inspect(packet, {showHidden: false, depth: null, colors: true}));
if (packet.type === "exec_halo") {
try {

4
docs/halo-bridge.md Normal file
View File

@@ -0,0 +1,4 @@
(TODO description of halo-bridge)
(TODO how to connect with halo-bridge from external web app - check HTTPS then fall back to HTTP endpoint,
would give nearly 100% coverage of all systems and browsers)

View File

@@ -1,20 +1,10 @@
const {JWEUtil} = require("../jwe_util");
const WebSocketAsPromised = require("websocket-as-promised");
const queryString = require("query-string");
let currentCmd = null;
let jweUtil = new JWEUtil();
let wsp;
function createWs(url) {
return new WebSocketAsPromised(url, {
packMessage: data => JSON.stringify(data),
unpackMessage: data => JSON.parse(data),
attachRequestId: (data, requestId) => Object.assign({uid: requestId}, data),
extractRequestId: data => data && data.uid
});
}
async function haloGateExecutorCreateWs(logCallback, newCommandCallback) {
let protocol;
let searchParts = window.location.hash.split('/');

88
web/web_utils.js Normal file
View File

@@ -0,0 +1,88 @@
const WebSocketAsPromised = require('websocket-as-promised');
function haloCreateWs(url) {
return new WebSocketAsPromised(url, {
packMessage: data => JSON.stringify(data),
unpackMessage: data => JSON.parse(data),
attachRequestId: (data, requestId) => Object.assign({uid: requestId}, data),
extractRequestId: data => data && data.uid
});
}
function runHealthCheck(url) {
return new Promise((resolve, reject) => {
let closeTimeout = null;
const pingUrl = url.includes('?') ? (url + '&ping=1') : (url + '?ping=1');
const wsp = new WebSocketAsPromised(pingUrl, {timeout: 5000});
wsp.onClose.addListener(event => {
if (event.code === 4090) {
if (closeTimeout) {
clearTimeout(closeTimeout);
}
resolve(url);
} else {
throw new Error("Unexpected WebSocket close code: " + event.code);
}
});
wsp.open()
.then(() => {
closeTimeout = setTimeout(() => {
wsp.close();
reject(new Error('WebSocket didn\'t close as expected.'));
}, 2000);
})
.catch((err) => {
reject(err);
})
});
}
function createChecks(wsPort, wssPort) {
return [
runHealthCheck('wss://halo-bridge.local:' + wssPort + '/ws'),
runHealthCheck('ws://127.0.0.1:' + wsPort + '/ws')
];
}
async function haloFindBridge(options) {
options = Object.assign({}, options) || {};
if (!options.wsPort) {
options.wsPort = 32868;
}
if (!options.wssPort) {
options.wssPort = 32869;
}
if (options.diagnose) {
let res = await Promise.allSettled(createChecks(wsPort, wssPort));
let urls = [];
let errors = [];
for (let o of res) {
if (o.status === "fulfilled") {
urls.push(o.value);
} else {
errors.push(o.reason);
}
}
return {
urls,
errors
};
} else {
try {
return await Promise.any(createChecks(wsPort, wssPort));
} catch (e) {
throw new Error("Unable to locate halo bridge.");
}
}
}
module.exports = {haloCreateWs, haloFindBridge};

View File

@@ -19,9 +19,9 @@ const {
arr2hex, hex2arr, parsePublicKeys, convertSignature, recoverPublicKey
} = require("../halo/utils");
const {__runTestSuite} = require("../halo/tests");
const WebSocketAsPromised = require("websocket-as-promised");
const {HaloGateway} = require("../halo/gateway/requestor");
const {haloGateExecutorCreateWs, haloGateExecutorUserConfirm} = require("../halo/gateway/executor");
const {haloFindBridge, haloCreateWs} = require("./web_utils");
module.exports = {
// utilities
@@ -33,6 +33,10 @@ module.exports = {
// for web usage
execHaloCmdWeb,
haloFindBridge,
// for bridge demo
haloCreateWs,
// for web usage with gateway
HaloGateway,