Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Feature: pgp flux app signer #1212

Open
wants to merge 18 commits into
base: development
Choose a base branch
from
1 change: 1 addition & 0 deletions ZelBack/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
server: {
allowedPorts: [16127, 16137, 16147, 16157, 16167, 16177, 16187, 16197],
apiport: 16127, // homeport is -1, ssl port is +1
appVerificationAddress: '169.254.42.42',
},
database: {
url: '127.0.0.1',
Expand Down
192 changes: 192 additions & 0 deletions ZelBack/src/services/appVerificationService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* GetNodeIdentity | NodeBApp -> NodeB (so other end can get public pgp key)
* NodeIdentitySent | NodeBApp -> NodeAApp
* ChallengeRequest | NodeAApp -> NodeA (contains target ip, apiport via identity)
* ChallengeCreated | NodeA (Gets pgp pubkey from ip apiport)
* ChallengeSent | NodeAApp -> NodeBApp
* ChallengeDecryptRequest | NodeBApp -> NodeB
* ChallengeDecrypted | NodeB
* DecryptedSent | NodeBApp -> NodeAApp
* Verified | NodeAApp has now verified NodeBApp
*/

const config = require('config');
const log = require('../lib/log');
const dockerService = require('./dockerService');
const serviceHelper = require('./serviceHelper');
const pgpSerivce = require('./pgpService');
const messageHelper = require('./messageHelper');
const daemonServiceUtils = require('./daemonService/daemonServiceUtils');
const generalService = require('./generalService');

const { randomBytes } = require('node:crypto');
const express = require('express');

let server = null;

async function generateChallengeMessage(req, res) {
const parsedBody = serviceHelper.ensureObject(req.body);
const { identity } = parsedBody;

if (!identity || identity.length !== 64) {
res.statusMessage = 'Authenticating node identity is required (txid)';
res.status(422).end();
return;
}

const app = await dockerService.getAppNameByContainerIp(req.socket.remoteAddress);
if (!app) {
res.statusMessage = 'You are not authorized for this endpoint';
res.status(403).end();
return;
}

const fluxnodeRes = await daemonServiceUtils.executeCall('listfluxnodes', [identity]);

if (!fluxnodeRes || fluxnodeRes.status !== 'success' || !fluxnodeRes.data.length) {
res.statusMessage = 'Unable to find node identity in deterministicfluxnodelist';
res.status(422).end();
return;
}

// check if more than one?!?
const fluxnode = fluxnodeRes.data[0];

// this is ridiculous having to do this all the time. The node identity should always include the port
const [ip, apiport] = fluxnode.ip.includes(':') ? fluxnode.ip.split(':') : [fluxnode.ip, '16127'];

const message = randomBytes(16).toString('hex');
const toEncrypt = JSON.stringify({ app, message });

// https://1-2-3-4-16127.node.api.runonflux.io/flux/pgp
const hyphenEncodedHostname = `${ip.split('.').join('-')}-${apiport}`;
const pgpEndpoint = `http://${hyphenEncodedHostname}.node.api.runonflux.io/flux/pgp`;

const { data: pgpPubKeyRes } = await serviceHelper.axiosGet(pgpEndpoint, { timeout: 2000 });

if (!pgpPubKeyRes?.status === 'success') {
res.statusMessage = 'Unable to retrieve pgp key for target';
res.status(422).end();
}

const encrypted = await pgpSerivce.encryptMessage(toEncrypt, [pgpPubKeyRes.data]);

const dataMessage = messageHelper.createDataMessage({ message, encrypted });
res.json(dataMessage);
}

async function getNodeIdentity(req, res) {
const app = await dockerService.getAppNameByContainerIp(req.socket.remoteAddress);
if (!app) {
res.statusMessage = 'You are not authorized for this endpoint';
res.status(403).end();
return;
}

let outPoint = null;
try {
// this is reliant on fluxd running
const collateral = await generalService.obtainNodeCollateralInformation();
outPoint = { txhash: collateral.txhash, outidx: collateral.txindex };
} catch {
log.error('Error getting collateral info from daemon.');
}

if (!outPoint) {
res.statusMessage = 'Unable to get node identity.. try again later';
res.status(503).end();
return;
}

const message = messageHelper.createDataMessage(outPoint);
res.json(message);
}

async function decryptChallengeMessage(req, res) {
const app = await dockerService.getAppNameByContainerIp(req.socket.remoteAddress);
if (!app) {
res.statusMessage = 'You are not authorized for this endpoint';
res.status(403).end();
return;
}

const parsedBody = serviceHelper.ensureObject(req.body);
const { encrypted } = parsedBody;

if (!encrypted) {
res.statusMessage = 'Encrypted message not provided';
res.status(422).end();
return;
}

// eslint-disable-next-line no-undef
const { pgpPrivateKey } = userconfig.initial;

if (!pgpPrivateKey) {
res.statusMessage = 'Pgp key not set';
res.status(500).end();
return;
}

const decrypted = await pgpSerivce.decryptMessage(encrypted, pgpPrivateKey);

if (!decrypted) {
res.statusMessage = 'Unable to decrypt message';
res.status(500).end();
}

const challenge = JSON.parse(decrypted);

if (challenge.app !== app) {
res.status(403).end();
return;
}

const dataMessage = messageHelper.createDataMessage(challenge.message);
res.json(dataMessage);
}

function handleError(middleware, req, res, next) {
middleware(req, res, (err) => {
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
res.statusMessage = err.message;
return res.sendStatus(400);
}
else if (err) {
log.error(err);
return res.sendStatus(400);
}

next();
});
}

function start() {
if (server) return;

const app = express();
app.use((req, res, next) => {
handleError(express.json(), req, res, next);
});
app.post('/createchallenge', generateChallengeMessage);
app.post('/decryptchallenge', decryptChallengeMessage);
app.get('/nodeidentity', getNodeIdentity);
app.all('*', (_, res) => res.status(404).end());

const bindAddress = config.server.appVerificationAddress;
server = app.listen(80, bindAddress, () => {
log.info(`Server listening on port: 80 address: ${bindAddress}`);
});
}

function stop() {
if (server) {
server.close();
server = null;
}
}

module.exports = {
start,
stop,
};
2 changes: 1 addition & 1 deletion ZelBack/src/services/daemonService/daemonServiceUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ let daemonCallRunning = false;
* To execute a remote procedure call (RPC).
* @param {string} rpc Remote procedure call.
* @param {string[]} params RPC parameters.
* @returns {object} Message.
* @returns {Promise<object>} Message.
*/
async function executeCall(rpc, params) {
const rpcparameters = params || [];
Expand Down
50 changes: 50 additions & 0 deletions ZelBack/src/services/dockerService.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,53 @@ async function getDockerContainerByIdOrName(idOrName) {
const dockerContainer = docker.getContainer(myContainer.Id);
return dockerContainer;
}

/**
*
* @returns {Promise<string[]>}
*/
async function getFluxDockerNetworkSubnets() {
const fluxNetworks = await docker.listNetworks({
filters: JSON.stringify({
name: ['fluxDockerNetwork'],
}),
});

const subnets = fluxNetworks.map((network) => network.IPAM.Config[0].Subnet);

return subnets;
}

async function getAppNameByContainerIp(ip) {
const fluxNetworks = await docker.listNetworks({
filters: JSON.stringify({
name: ['fluxDockerNetwork'],
}),
});

const fluxNetworkNames = fluxNetworks.map((n) => n.Name);

const networkPromises = [];
fluxNetworkNames.forEach((networkName) => {
const dockerNetwork = docker.getNetwork(networkName);
networkPromises.push(dockerNetwork.inspect());
});

const fluxNetworkData = await Promise.all(networkPromises);

let appName = null;
// eslint-disable-next-line no-restricted-syntax
for (const fluxNetwork of fluxNetworkData) {
const subnet = fluxNetwork.IPAM.Config[0].Subnet;
if (serviceHelper.ipInSubnet(ip, subnet)) {
appName = fluxNetwork.Name.split('_')[1];
break;
}
}

return appName;
}

/**
* Returns low-level information about a container.
*
Expand Down Expand Up @@ -587,6 +634,7 @@ async function appDockerCreate(appSpecifications, appName, isComponent, fullAppS
'max-size': '20m',
},
},
ExtraHosts: [`app.identity.service:${config.server.appVerificationAddress}`],
},
};

Expand Down Expand Up @@ -972,6 +1020,8 @@ module.exports = {
createFluxDockerNetwork,
getDockerContainerOnly,
getDockerContainerByIdOrName,
getFluxDockerNetworkSubnets,
getAppNameByContainerIp,
createFluxAppDockerNetwork,
removeFluxAppDockerNetwork,
pruneNetworks,
Expand Down
Loading