'use strict';
/**
* @module lib/protocol/authenticate
* @summary Whiteflag authentication module
* @description Module for Whiteflag authentication
* @tutorial modules
* @tutorial protocol
*/
module.exports = {
message: verifyMessage,
verify: verifyAuthentication,
remove: removeAuthentication,
sign: createSignature,
decodeSignature,
verifySignature,
generateToken
};
/* Type definitions */
/**
* A Whiteflag authentication signature object
* @typedef {Object} wfSignature
* @property {string} protected Encoded signature header to identify which algorithm is used to generate the signature
* @property {string} payload Encoded payload with the information as defined in the Whiteflag protocol specification
* @property {string} signature The digital signature validating the information contained in the payload
*/
/**
* A Whiteflag decoded authentication signature object
* @typedef {Object} wfSignDecoded
* @property {Object} header Signature header to identify which algorithm is used to generate the signature
* @property {wfSignPayload} payload Payload object of a Whiteflag authentication signature
* @property {string} signature The digital signature validating the information contained in the payload
*/
/**
* A Whiteflag authentication signature payload object
* @typedef {Object} wfSignPayload
* @property {string} addr The blockchain address used to send the corresponding `A1` message and of which the corresponding private key is used to create the signature
* @property {string} orgname The name of the originator, which can be chosen freely
* @property {string} url The same URL as in the `VerificationData` field of the corresponding `A1` message
* @property {string} extpubkey The serialised extended parent public key from which the child public keys and addresses used by this originator can be derived (currently not supported)
*/
/**
* A Whiteflag extended authentication signature object
* @typedef {Object} wfExtSignature
* @property {string} blockchain the name of the blockchain
* @property {string} pubkey the blockchain account public key of the originator
* @property {wfSignature} jws a Whiteflag authentication JWS object
*/
/* Common internal functions and classes */
const log = require('../_common/logger');
const arr = require('../_common/arrays');
const req = require('../_common/request');
const jws = require('../_common/jws');
const { type } = require('./_common/messages');
const { noHexPrefix } = require('../_common/format');
const { hexToBuffer } = require('../_common/encoding');
const { ProcessingError,
ProtocolError } = require('../_common/errors');
/* Whiteflag modules */
const wfBlockchains = require('../blockchains');
const wfCrypto = require('./crypto');
const wfState = require('./state');
/* Whiteflag configuration data */
const wfConfigData = require('./config').getConfig();
/* Module constants */
const MODULELOG = 'authenticate';
const AUTHMESSAGECODE = 'A';
/* MAIN MODULE FUNCTIONS */
/**
* Checks if message can be authenticated and updates metaheader accordingly
* @function verifyMessage
* @alias module:lib/protocol/authenticate.message
* @param {wfMessage} wfMessage a Whiteflag message
* @param {wfMessageCb} callback function called on completion
*/
function verifyMessage(wfMessage, callback) {
let { MetaHeader: meta } = wfMessage;
// Lookup originator in state
wfState.getOriginatorData(meta.originatorAddress, function verifyOrigGetDataCb(err, originator) {
if (err) return callback(err, wfMessage);
// Originator found in state
if (originator && meta.originatorAddress.toLowerCase() === originator.address.toLowerCase()) {
if (originator.authValid) {
meta.originatorValid = true;
return callback(null, wfMessage);
}
}
// Originator not found in state or no valid authentication data
if (wfConfigData.authentication.strict) {
meta.originatorValid = false;
}
return callback(null, wfMessage);
});
}
/**
* Checks the authentication information of the message originator and updates metaheader accordingly
* @function verifyAuthentication
* @alias module:lib/protocol/authenticate.verify
* @param {wfMessage} wfAuthMessage a Whiteflag authentication message
* @param {wfMessageCb} callback function called on completion
*/
function verifyAuthentication(wfAuthMessage = {}, callback) {
if (wfAuthMessage?.MessageHeader?.MessageCode !== AUTHMESSAGECODE) {
return callback(new ProcessingError(`Not an authentication message: ${type(wfAuthMessage)} message`), wfAuthMessage);
}
// Check indicator for authentication type
switch (wfAuthMessage.MessageBody.VerificationMethod) {
case '1': {
// Digital Signature
return wfAuthentication1(wfAuthMessage, callback);
}
case '2': {
// Shared Token
return wfAuthentication2(wfAuthMessage, callback);
}
default: {
// Method does not exist
return callback(new ProtocolError(`Invalid authentication method: ${type(wfAuthMessage)}`));
}
}
}
/**
* Removes the authentication information of the message originator
* @function removeAuthentication
* @alias lib/protocol/authenticate.remove
* @param {wfMessage} wfAuthMessage a Whiteflag authentication message
* @param {wfMessageCb} callback function called on completion
*/
function removeAuthentication(wfAuthMessage = {}, callback) {
let {
MetaHeader: meta,
MessageHeader: header } = wfAuthMessage;
// Check messageheader
if (wfAuthMessage?.MessageHeader?.MessageCode !== AUTHMESSAGECODE) {
return callback(new ProcessingError(`Cannot remove authentication: Not an authentication message: ${type(wfAuthMessage)}`), wfAuthMessage);
}
if (
header?.ReferenceIndicator !== '1'
&& header?.ReferenceIndicator !== '4'
) {
return callback(new ProcessingError(`Cannot remove authentication: ${type(wfAuthMessage)} message does not have reference code 1 or 4`), wfAuthMessage);
}
// Check for referenced message in originators state
wfState.getOriginatorData(meta.originatorAddress, function authGetOriginatorCb(err, originator) {
if (err) log.error(MODULELOG, `Error getting originator state: ${err.message}`);
// Check originator
if (!originator) {
// Authentication message from previously unknown originator
log.debug(MODULELOG, `Cannot remove authentication: ${type(wfAuthMessage)} message is from unknown originator: ${meta.originatorAddress}`);
meta.validationErrors = arr.addArray(meta.validationErrors, 'Unknown originator');
return callback(null, wfAuthMessage);
}
// Check if any known authentication messages
if (!originator.authMessages) {
log.debug(MODULELOG, `Cannot process ${type(wfAuthMessage)} message: No authentication messages known for originator: ${originator.address}`);
return callback(null, wfAuthMessage);
}
// Authentication message from known originator
meta.originatorValid = true;
const authIndex = originator.authMessages.findIndex(hash => hash === header.ReferencedMessage);
if (authIndex >= 0) {
log.debug(MODULELOG, `Removing authentication message from originators state: ${originator.authMessages[authIndex]}`);
originator.authMessages.splice(authIndex, 1);
originator.updated = new Date().toISOString();
wfState.upsertOriginatorData(originator);
}
// Check if any authentication message transaction hashes left
if (originator.authMessages.length === 0) {
log.debug(MODULELOG, `No valid authentication messages anymore for originator: ${originator.address}`);
originator.authValid = false;
originator.updated = new Date().toISOString();
wfState.upsertOriginatorData(originator);
}
return callback(null, wfAuthMessage);
});
}
/**
* Requests verification data of an authentication token for the specified blockchain address
* @function generateToken
* @alias module:lib/protocol/authenticate.generateToken
* @param {string} authToken the secret authentication token in hexadecimal
* @param {string} address the address of the account for which the signature is requested
* @param {string} blockchain the name of the blockchain
* @param {genericCb} callback function called on completion
*/
function generateToken(authToken, address, blockchain, callback) {
let tokenBuffer = hexToBuffer(noHexPrefix(authToken));
authToken = null;
// Get blockchain address is binary
wfBlockchains.getBinaryAddress(address, blockchain, function authGetAddressCb(err, addressBuffer) {
if (err) return callback(err, null);
// Generate authentication token and compare with message
wfCrypto.getTokenVerificationData(tokenBuffer, addressBuffer, function authGenerateTokenCb(err, dataBuffer) {
if (err) return callback(err, null);
return callback(null, dataBuffer.toString('hex').toLowerCase());
});
});
}
/**
* Creates a authentication signature for the appropriate blockchain
* @function createSignature
* @alias module:lib/protocol/authenticate.sign
* @param {wfSignPayload} signPayload the signature payload object to be signed
* @param {string} address the address of the account for which the signature is requested
* @param {string} blockchain the blockchain for which the signature is requested
* @param {requestSignatureCb} callback function called on completion
*/
function createSignature(signPayload, address, blockchain, callback) {
// Check blockchain and address
if (!blockchain || !address) {
return callback(new ProcessingError('Missing blockchain or address', null, 'WF_API_BAD_REQUEST'));
}
// Check request for complete signature payload
let signErrors = [];
if (!signPayload?.addr) signPayload.addr = address;
if (signPayload.addr !== address) signErrors.push('Signature address does not match blockchain account');
arr.addArray(signErrors, checkSignPayload(signPayload));
if (signErrors.length > 0) {
return callback(new ProtocolError('Invalid Whiteflag authentication signature request', signErrors, 'WF_SIGN_ERROR'), null);
}
/**
* @callback authRequestSignatureCb
* @param {Error} err any error
* @param {wfSignature} wfSignature the Whiteflag JWS to be signs
*/
wfBlockchains.requestSignature(signPayload, blockchain, function authRequestSignatureCb(err, wfSignature) {
if (err) return callback(err);
decodeSignature(wfSignature, function authDecodeSignatureCb(err, wfSignDecoded) {
if (err) return callbacl(err);
return callback(null, wfSignature, wfSignDecoded);
});
});
}
/**
* Decodes authentication signature
* @function decodeSignature
* @alias module:lib/protocol/authenticate.decodeSignature
* @param {wfSignature} signature a Whiteflag authentication signature
* @param {genericCb} callback function called on completion
*/
function decodeSignature(wfSignature, callback) {
try {
const wfSignDecoded = jws.decode(wfSignature);
return callback(null, wfSignDecoded);
} catch(err) {
if (err.causes?.length > 0) return callback(new ProcessingError('Cannot decode Whiteflag authentication signature', err.causes));
return callback(new ProcessingError('Cannot decode Whiteflag authentication signature', err.message));
}
}
/**
* Verifies authentication signature
* @function verifySignature
* @alias module:lib/protocol/authenticate.verifySignature
* @param {wfExtSignature} wfExtSignature an extended Whiteflag authentication signature
* @param {endpointSignatureVerifyCb} callback function called on completion
*/
function verifySignature(wfExtSignature, callback) {
// Check properties of the extended signature object
let signErrors = [];
if (!wfExtSignature?.blockchain) signErrors.push('Missing blockchain property: blockchain');
if (!wfExtSignature?.pubkey) signErrors.push('Missing originator public key property: pubkey');
if (!wfExtSignature?.jws) signErrors.push('Missing Whiteflag authentication signature property: jws');
if (signErrors.length > 0) {
return callback(new ProcessingError('Invalid extended Whiteflag authentication signature data', signErrors, 'WF_API_BAD_REQUEST'));
}
// Decode signature and check properties of the payload
const wfSignature = wfExtSignature.jws;
decodeSignature(wfSignature, function(err, wfSignDecoded){
if (err) return callback (err);
signErrors = checkSignPayload(wfSignDecoded.payload);
if (signErrors.length > 0) {
return callback(new ProtocolError('Invalid Whiteflag authentication signature', signErrors, 'WF_SIGN_ERROR'), null);
}
const blockchain = wfExtSignature.blockchain;
const signAddress = wfSignDecoded.payload.addr;
const orgPubkey = wfExtSignature.pubkey;
/**
* @callback authVerifySignatureCb
* @param {Error} err any error
* @param {wfSignDecoded} [result] the verified and decoded signature
*/
wfBlockchains.verifySignature(wfSignature, signAddress, orgPubkey, blockchain, function authVerifySignatureCb(err, result) {
if (err) return callback(err);
if (result) return callback(null, wfSignDecoded);
return callback(null, wfSignDecoded);
});
});
}
/* PRIVATE MODULE FUNCTIONS */
/**
* Verifies the information for authentication method 1
* @private
* @param {Object} wfAuthMessage a Whiteflag authentication message
* @param {wfMessageCb} callback function called on completion
*/
function wfAuthentication1(wfAuthMessage, callback) {
let authURL;
let validDomains = [];
let {
MetaHeader: meta,
MessageHeader: header,
MessageBody: body } = wfAuthMessage;
// Get URL in authentication message and from valid domain list in config file
try {
// eslint-disable-next-line no-undef
authURL = new URL(body.VerificationData);
validDomains = wfConfigData.authentication['1'].validDomains || [];
} catch(err) {
log.warn(MODULELOG, `Could not get signature URLs: ${err.message}`);
meta.validationErrors = arr.addItem(
meta.validationErrors,
`Could not get signature URLs: ${err.message}`
);
return callback(null, wfAuthMessage);
}
// Check for valid domain names
if (validDomains.length > 0 && !validDomains.includes(authURL.hostname)) {
meta.validationErrors = arr.addItem(
meta.validationErrors,
`The domain that holds the authentication signature is not considered valid: ${authURL.hostname}`
);
meta.originatorValid = false;
return callback(null, wfAuthMessage);
}
// Check availability of public key
if (!meta?.originatorPubKey) {
return callback(new Error(`No public key available for authentication of originator of ${type(wfAuthMessage)} message: ${meta.transactionHash}`));
}
// Get signature from an internet resource
retrieveSignature(authURL, function authGetSignatureCb(err, wfSignature) {
if (err) {
log.warn(MODULELOG, `Could not retrieve authentication signature: ${err.message}`);
meta.validationErrors = arr.addItem(
meta.validationErrors,
`Could retrieve get signature: ${err.message}`);
return callback(err, wfAuthMessage);
}
// Verify the signature
const wfExtSignature = {
blockchain: meta.blockchain,
address: meta.originatorAddress,
pubkey: meta.originatorPubKey,
jws: wfSignature
};
verifySignature(wfExtSignature, function authVerifySignatureCb(err, wfSignDecoded) {
if (err && !(err instanceof ProtocolError)) {
log.warn(MODULELOG, `Could not verify signature from ${body.VerificationData}: ${err.message}`);
meta.validationErrors = arr.addItem(
meta.validationErrors,
`Could not verify signature: ${err.message}`
);
return callback(null, wfAuthMessage);
}
if (!wfSignDecoded) {
log.debug(MODULELOG, `Could not verify signature from ${body.VerificationData}`);
meta.validationErrors = arr.addItem(
meta.validationErrors,
'Could not decode and validate signature'
);
return callback(null, wfAuthMessage);
}
log.debug(MODULELOG, `Verified signature from ${body.VerificationData}: ` + JSON.stringify(wfSignDecoded));
const { payload } = wfSignDecoded;
// Perform authentication checks
let authErrors = [];
if (err instanceof ProtocolError) {
if (err.causes?.length > 0) {
authErrors = arr.addArray(authErrors, err.causes);
} else {
authErrors.push(err.message);
}
}
if (payload.addr.toLowerCase() !== meta.originatorAddress.toLowerCase()) {
authErrors.push(`Signature address does not correspond with message address: ${payload.addr} != ${meta.originatorAddress}`);
meta.originatorValid = false;
}
if (payload.url !== body.VerificationData) {
authErrors.push(`Signature URL does not correspond with authentication message URL: ${payload.url} != ${body.VerificationData}`);
meta.originatorValid = false;
}
// Check result and update metaheader and state
if (authErrors.length > 0) {
// Return with authentication errors
meta.originatorValid = false;
let err = new ProtocolError(`Could not authenticate originator of ${type(wfAuthMessage)} message: ${meta.transactionHash}`, authErrors, 'WF_AUTH_ERROR');
return callback(err, wfAuthMessage);
}
// Authentication is valid
meta.originatorValid = true;
// Update originator state
let originatorData = {
name: payload.orgname,
blockchain: meta.blockchain,
address: meta.originatorAddress,
publicKey: meta.originatorPubKey,
url: payload.url,
updated: new Date().toISOString(),
authValid: true,
authMessages: [ meta.transactionHash ]
};
if (header.ReferenceIndicator === '0') {
originatorData.authMessages = [ meta.transactionHash ];
}
log.info(MODULELOG, `Updating state with authenticated originator of ${type(wfAuthMessage)} message: ${meta.transactionHash}: ` + JSON.stringify(originatorData));
originatorData.updated = new Date().toISOString();
wfState.upsertOriginatorData(originatorData);
return callback(null, wfAuthMessage);
});
});
}
/**
* Verifies the information for authentication method 2
* @private
* @param {Object} wfAuthMessage a Whiteflag authentication message
* @param {wfMessageCb} callback
*/
function wfAuthentication2(wfAuthMessage, callback) {
let {
MetaHeader: meta,
MessageHeader: header,
MessageBody: body } = wfAuthMessage;
// Get known authentication tokens and iterate over them
wfState.getKeyIds('authTokens', function authGetKeyIdsCb(err, authTokenIds) {
if (err) return callback(err, wfAuthMessage);
iterateAuthTokens(authTokenIds, 0);
});
/**
* Tries to match a known authentication token with the received authentication message
* @private
* @param {Array} authTokenIds Identifiers of all known authentication tokens
* @param {number} t Token counter
*/
function iterateAuthTokens(authTokenIds = [], t = 0) {
// Return message if no more tokens
if (t >= authTokenIds.length) {
log.debug(MODULELOG, `Unknown originator authentication token in ${type(wfAuthMessage)} message: ${meta.transactionHash}`);
meta.validationErrors = arr.addItem(
meta.validationErrors,
'Unknown originator authentication token'
);
return callback(null, wfAuthMessage);
}
log.trace(MODULELOG, `Trying authentication token ${(t + 1)}/${authTokenIds.length} for ${type(wfAuthMessage)} message: ${meta.transactionHash} from ${meta.blockchain} account ${meta.originatorAddress}`);
// Get secret authentication token from state and put in buffer
wfState.getKey('authTokens', authTokenIds[t], function authGetKey(err, authToken) {
if (err) return callback(err, wfAuthMessage);
// Generate authentication token and compare with message
generateToken(authToken, meta.originatorAddress, meta.blockchain, function authGenerateTokenCb(err, token) {
if (err) return callback(err, wfAuthMessage);
// Comnpare message authentication data with known token
if (token !== body.VerificationData.toLowerCase()) {
// No match; try next token
return iterateAuthTokens(authTokenIds, (t + 1));
}
log.trace(MODULELOG, `Found a matching authentication token for ${type(wfAuthMessage)} message: ${meta.transactionHash}`);
meta.originatorValid = true;
// Get originator data and update originator state
wfState.getOriginatorAuthToken(authTokenIds[t], function authGetOriginatorTokenCb(err, originator) {
if (err) return callback(err, wfAuthMessage);
let name = '(unknown)';
if (originator?.name) name = originator.name;
let originatorData = {
name: name,
blockchain: meta.blockchain,
address: meta.originatorAddress,
publicKey: meta.originatorPubKey,
authTokenId: authTokenIds[t],
updated: new Date().toISOString(),
authValid: true
};
if (header.ReferenceIndicator === '0') {
originatorData.authMessages = [ meta.transactionHash ];
}
log.debug(MODULELOG, `Updating state with authenticated originator of ${type(wfAuthMessage)} message: ${meta.transactionHash}: ` + JSON.stringify(originatorData));
wfState.upsertOriginatorData(originatorData);
return callback(null, wfAuthMessage);
});
});
});
}
}
/**
* Gets an authentication signature from an url
* @private
* @param {URL} authURL a URL to get the authentication signature from
* @param {genericCb} callback function called on completion
*/
function retrieveSignature(authURL, callback) {
req.httpRequest(authURL)
.then(wfSignature => {
return callback(null, wfSignature);
})
.catch(err => {
return callback(new Error(`Error retrieving signature from ${authURL.origin}: ${err.message}`));
})
}
/**
* @private
* @param {Object} signPayload the signature payload to be checked
* @returns {Array} error messages, if any
*/
function checkSignPayload(signPayload) {
let signErrors = [];
if (!signPayload?.addr) signErrors.push('Missing authentication signature property: \'addr\'');
if (!signPayload?.orgname) signErrors.push('Missing authentication signature property: \'orgname\'');
if (!signPayload?.url) signErrors.push('Missing authentication signature property: \'url\'');
return signErrors;
}