380 lines
14 KiB
JavaScript
380 lines
14 KiB
JavaScript
// Load modules.
|
|
var passport = require('passport-strategy');
|
|
let bip32 = require('bip32');
|
|
const util = require('util');
|
|
const crypto = require('crypto');
|
|
|
|
/**
|
|
* Creates an instance of `HDAuthStrategy`.
|
|
*
|
|
* The Hierarchachally Determinstic Authentication strategy authenticates requests using the .
|
|
*
|
|
* * Applications must supply a `verify` callback, for which the function
|
|
* signature is:
|
|
*
|
|
* function(xpub, challengeRequestDerivationPath, challengeRequestMessage, signature, done) { ... }
|
|
*
|
|
* Function Parameters:
|
|
* - `xpub` required: Hierarchachally deterministic extended public key which will be used to generate public keys that we are gonna use to authenticate requests.
|
|
* - `challengeRequestDerivationPath` optional: derivation path which will be used to derive the public key that we need to authenticate request
|
|
* - `challengeRequestMessage` required: message which will be hashed and verified to authenticate a request
|
|
* - `challengeResponseSignature` required: expected signature which will be verified to authenticate a request
|
|
* - `done` callback with the following arguments:
|
|
*
|
|
* done(err, user, info);
|
|
*
|
|
* if the challenge isn't successfully responded to, `user` should be set to false to indicate an authentication failure. Additional challenge response `info` can be optionally passed as a third argument (which will be set by Passport at `req.authInfo` which will be accessible to later middleware for access control).
|
|
*
|
|
* Options:
|
|
* - `serverExtendedPrivateKey` which will be used to sign requests which this server sends to clients so that clients can authenticate that they are communicating with a server that is owned/created/controlled by the service.
|
|
* - `serverExtendedPrivateKeyDerivationPath` derivation path which was used to produce this servers extended private key (rooted at the serviceAuthenticatingExtendedPublicKey). This will be used as the prefix for the verificationDerivationPath that the servers are gonna send clients to verify request are made by a server owned/created/controlled by the service
|
|
* - `serviceAuthenticatingExtendedPublicKey` xpub which clients can use to verify every authenticated request made by the service.
|
|
*
|
|
* Examples:
|
|
*
|
|
* passport.use(new HDAuthStrategy({
|
|
* serverExtendedPrivateKey: 'xprivServerExtendedPrivatekey', // used to sign request made by this server...
|
|
* serverExtendedPrivateKeyDerivationPath: "a/395'/2349'/4252"
|
|
* serviceAuthenticatingExtendedPublicKey: "xpubServiceExtendedPublicKey",
|
|
* },
|
|
* function(xpub, challengeRequestDerivationPath, challengeRequestMessage, challengeResponseSignature, done) {
|
|
* User.findByXpub({ xpub: xpub }, function (err, user) {
|
|
* if (err) { return done(err); }
|
|
* if (!user) { return done(null, false); }
|
|
* return done(null, user, { verified: true });
|
|
* });
|
|
* }
|
|
* ));
|
|
*
|
|
* @constructor
|
|
* @param {Object} options
|
|
* @param {Function} verify
|
|
* @api public
|
|
*/
|
|
function HDAuthStrategy(options, verify) {
|
|
if (typeof options == 'function') {
|
|
verify = options;
|
|
options = undefined;
|
|
}
|
|
options = options || {};
|
|
|
|
if (!verify || typeof verify != 'function') {
|
|
throw new TypeError('HDAuthStrategy requires a verify callback');
|
|
}
|
|
|
|
if (!options.serverExtendedPrivateKey) {
|
|
throw new TypeError('HDAuthStrategy requires a serverExtendedPrivateKey option');
|
|
}
|
|
|
|
if (!options.serverExtendedPrivateKeyDerivationPath) {
|
|
throw new TypeError('HDAuthStrategy requires a serverExtendedPrivateKeyDerivationPath option');
|
|
}
|
|
|
|
if (!options.serviceAuthenticatingExtendedPublicKey) {
|
|
throw new TypeError('HDAuthStrategy requires a serviceAuthenticatingExtendedPublicKey option');
|
|
}
|
|
|
|
passport.Strategy.call(this);
|
|
|
|
this.name = 'hd-auth';
|
|
this._verify = verify;
|
|
|
|
this._serverExtendedPrivateKey = options.serverExtendedPrivateKey;
|
|
this._serverExtendedPrivateKeyDerivationPath = options.serverExtendedPrivateKeyDerivationPath;
|
|
this._serviceAuthenticatingExtendedPublicKey = options.serviceAuthenticatingExtendedPublicKey;
|
|
|
|
this._passReqToCallback = options.passReqToCallback;
|
|
}
|
|
|
|
// Inherit from `passport.Strategy`.
|
|
util.inherits(HDAuthStrategy, passport.Strategy);
|
|
|
|
|
|
/**
|
|
* Authenticate request by delegating to a service provider using OAuth 2.0.
|
|
*
|
|
* Authenticate request by posting/verifying a challenge to a client
|
|
*
|
|
* @param {Object} req
|
|
* @api protected
|
|
*/
|
|
HDAuthStrategy.prototype.authenticate = function(req) {
|
|
var self = this;
|
|
|
|
self._extractChallengeFromRequest(req)
|
|
.then(challenge => {
|
|
function verified(err, user, info) {
|
|
if (err) { return self.error(err); }
|
|
if (!user) {
|
|
if (typeof info == 'string') {
|
|
info = { message: info }
|
|
}
|
|
info = info || {};
|
|
return self.fail(self.createChallenge(challenge.xpub, 'xpub_not_found', info.message));
|
|
}
|
|
// Verify `challengeResponseSignature`
|
|
if(self.verifyHDAuthChallengeResponse(challenge)) {
|
|
self.success(user, info);
|
|
} else {
|
|
return self.fail(self.createChallenge(challenge.xpub));
|
|
}
|
|
}
|
|
|
|
if (self._passReqToCallback) {
|
|
self._verify(req, challenge.xpub, challenge.derivationPath, challenge.message, challenge.response.signature, verified);
|
|
} else {
|
|
self._verify(challenge.xpub, challenge.derivationPath, challenge.message, challenge.response.signature, verified);
|
|
}
|
|
}).catch(error => {
|
|
if (error.xpub) {
|
|
return self.fail(self.createChallenge(error.xpub, error.code));
|
|
}
|
|
|
|
self.fail(error.code ? error.code : 400);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Consume request to produce hdAuth object
|
|
*
|
|
* @param {Object} request
|
|
* @api private
|
|
*/
|
|
HDAuthStrategy.prototype._extractChallengeFromRequest = function(request) {
|
|
return new Promise((resolve, reject) => {
|
|
// Load hdAuth via a promise...
|
|
if (request.headers
|
|
&& request.headers["hd-auth-challenge-xpub"]
|
|
&& request.headers["hd-auth-challenge-message"]
|
|
&& request.headers["hd-auth-challenge-request-derivation-path"]
|
|
&& request.headers["hd-auth-challenge-request-signature"]
|
|
&& request.headers["hd-auth-challenge-request-signature-derivation-path"]
|
|
&& request.headers["hd-auth-challenge-response-signature"]) {
|
|
resolve({
|
|
xpub: request.headers["hd-auth-challenge-xpub"],
|
|
message: request.headers["hd-auth-challenge-message"],
|
|
derivationPath: request.headers["hd-auth-challenge-request-derivation-path"],
|
|
request: {
|
|
signature: request.headers["hd-auth-challenge-request-signature"],
|
|
signatureDerivationPath: request.headers["hd-auth-challenge-request-signature-derivation-path"],
|
|
},
|
|
response: {
|
|
signature: request.headers["hd-auth-challenge-response-signature"]
|
|
}
|
|
})
|
|
} else if (request.headers
|
|
&& (request.headers["hd-auth-challenge-xpub"]
|
|
|| request.headers["hd-auth-challenge-message"]
|
|
|| request.headers["hd-auth-challenge-request-derivation-path"]
|
|
|| request.headers["hd-auth-challenge-request-signature"]
|
|
|| request.headers["hd-auth-challenge-request-signature-derivation-path"]
|
|
|| request.headers["hd-auth-challenge-response-signature"])) {
|
|
// incomplete hdAuth object
|
|
reject(400);
|
|
}
|
|
|
|
if (request.body
|
|
&& request.body
|
|
&& request.body.challenge.xpub
|
|
&& request.body.challenge.message
|
|
&& request.body.challenge.derivationPath
|
|
&& request.body.challenge.request
|
|
&& request.body.challenge.request.signature
|
|
&& request.body.challenge.request.signatureDerivationPath
|
|
&& request.body.challenge.response
|
|
&& request.body.challenge.response.signature) {
|
|
return resolve({
|
|
xpub: request.body.challenge.xpub,
|
|
message: request.body.challenge.message,
|
|
derivationPath: request.body.challenge.derivationPath,
|
|
request: {
|
|
signature: request.body.challenge.request.signature,
|
|
signatureDerivationPath: request.body.challenge.request.signatureDerivationPath,
|
|
},
|
|
response: {
|
|
signature: request.body.challenge.response.signature
|
|
}
|
|
})
|
|
} else if(request.body
|
|
&& request.body.challenge
|
|
&& (request.body.challenge.xpub
|
|
|| request.body.challenge.message
|
|
|| request.body.challenge.derivationPath
|
|
|| request.body.challenge.request
|
|
|| request.body.challenge.request.signature
|
|
|| request.body.challenge.request.signatureDerivationPath
|
|
|| request.body.challenge.response
|
|
|| request.body.challenge.response.signature)) {
|
|
// incomplete hdAuth object
|
|
return reject(400);
|
|
}
|
|
|
|
if (request.query
|
|
&& request.query
|
|
&& request.query.challenge.xpub
|
|
&& request.query.challenge.xpub
|
|
&& request.query.challenge.message
|
|
&& request.query.challenge.derivationPath
|
|
&& request.query.challenge.request
|
|
&& request.query.challenge.request.signature
|
|
&& request.query.challenge.request.signatureDerivationPath
|
|
&& request.query.challenge.response
|
|
&& request.query.challenge.response.signature) {
|
|
return resolve({
|
|
xpub: request.query.challenge.xpub,
|
|
message: request.query.challenge.message,
|
|
derivationPath: request.query.challenge.derivationPath,
|
|
request: {
|
|
signature: request.query.challenge.request.signature,
|
|
signatureDerivationPath: request.query.challenge.request.signatureDerivationPath,
|
|
},
|
|
response: {
|
|
signature: request.query.challenge.response.signature
|
|
}
|
|
})
|
|
} else if(request.query
|
|
&& request.query.hdAuth
|
|
&& request.query.hdAuth.challenge
|
|
&& (request.query.hdAuth.challenge.xpub
|
|
|| request.query.hdAuth.challenge.message
|
|
|| request.query.hdAuth.challenge.derivationPath
|
|
|| request.query.hdAuth.challenge.request
|
|
|| request.query.hdAuth.challenge.request.signature
|
|
|| request.query.hdAuth.challenge.request.signatureDerivationPath
|
|
|| request.query.hdAuth.challenge.response
|
|
|| request.query.hdAuth.challenge.response.signature)) {
|
|
// incomplete hdAuth object
|
|
return reject(400);
|
|
}
|
|
|
|
reject(400)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Create a authentication challenge for a specific xpub.
|
|
*
|
|
* {
|
|
* xpub: "xpubthingtocreatebip32tothethings",
|
|
* message: "message which will be signed by user and created by server.",
|
|
* derivationPath: "c/321/654/987", //
|
|
* request: {
|
|
* signature: "signature of the challenge.message from the server that created the challenge."
|
|
* signatureDerivationPath: "a/987/654/321"
|
|
* },
|
|
* response: {
|
|
* signature: "signature of challenge.message from the client that is responding to the challenge."
|
|
* }
|
|
* }
|
|
* @api public
|
|
*/
|
|
HDAuthStrategy.prototype.createChallenge = function(xpub, message, desc, uri) {
|
|
let challengeRequestDerivationPath = `c/${this.randomDerivationPath()}`;
|
|
|
|
if(!message) {
|
|
message = crypto.randomBytes(4).toString('hex');
|
|
}
|
|
|
|
return this._signChallenge({
|
|
// keys must be alphabetically ordered...
|
|
derivationPath: challengeRequestDerivationPath,
|
|
message: message,
|
|
xpub: xpub, // Use auth-identifier
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sign challenge
|
|
*
|
|
* @api private
|
|
*/
|
|
HDAuthStrategy.prototype._signChallenge = function(challenge) {
|
|
const randomDerivationPath = this.randomDerivationPath();
|
|
var signatureDerivationPath = `${this._serverExtendedPrivateKeyDerivationPath}/${randomDerivationPath}`;
|
|
|
|
challenge.request = {
|
|
signature: this.signMessageWithServerExtendedPrivateKey(JSON.stringify(challenge), randomDerivationPath),
|
|
signatureDerivationPath: signatureDerivationPath
|
|
}
|
|
|
|
return challenge;
|
|
};
|
|
|
|
HDAuthStrategy.prototype.signMessageWithServerExtendedPrivateKey = function(message, derivationPath) {
|
|
const serverNode = bip32.fromBase58(this._serverExtendedPrivateKey);
|
|
var serverSigningNode = serverNode.derivePath(derivationPath);
|
|
|
|
// TODO: Check if message is already a hash so we don't rehash this...
|
|
var hash = crypto.createHash('sha256').update(message).digest();
|
|
|
|
const signature = serverSigningNode.sign(hash).toString('hex');
|
|
return signature;
|
|
}
|
|
|
|
HDAuthStrategy.prototype.verifyHDAuthChallengeResponse = function(challenge) {
|
|
console.log("Trying to verify: ", challenge);
|
|
|
|
// Verify challenge request was produced by the service
|
|
if (
|
|
this._verifyMessage(
|
|
this._serviceAuthenticatingExtendedPublicKey,
|
|
challenge.request.signatureDerivationPath,
|
|
JSON.stringify(this._unsignedChallenge(challenge)),
|
|
challenge.request.signature
|
|
)) {
|
|
console.log("Message was produced by our service")
|
|
return this._verifyMessage(
|
|
challenge.xpub,
|
|
challenge.derivationPath,
|
|
challenge.message,
|
|
challenge.response.signature
|
|
);
|
|
} else {
|
|
console.error("Couldn't verify that service was produced by this service");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
HDAuthStrategy.prototype._unsignedChallenge = function(challenge) {
|
|
return {
|
|
// keys must be alphabetically ordered
|
|
derivationPath: challenge.derivationPath,
|
|
message: challenge.message,
|
|
xpub: challenge.xpub
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify that base58 key has signed a message
|
|
*
|
|
* @api private
|
|
*/
|
|
HDAuthStrategy.prototype._verifyMessage = function(base58Key, derivationPath, message, signature) {
|
|
|
|
if(derivationPath.startsWith("a/")) {
|
|
derivationPath = derivationPath.split("a/")[1];
|
|
}
|
|
|
|
if(derivationPath.startsWith("c/")) {
|
|
derivationPath = derivationPath.split("c/")[1];
|
|
}
|
|
|
|
const verificationNode = bip32.fromBase58(base58Key).derivePath(derivationPath);
|
|
const hash = crypto.createHash('sha256').update(message, 'utf8').digest();
|
|
|
|
return verificationNode.verify(hash, Buffer.from(signature, 'hex'));
|
|
}
|
|
|
|
HDAuthStrategy.prototype.randomDerivationPath = function() {
|
|
var randomNumbers = [];
|
|
|
|
for(var i = 0; i < 3; i++) {
|
|
randomNumbers.push(parseInt(crypto.randomBytes(3).toString('hex'), 16))
|
|
}
|
|
|
|
return randomNumbers.join('/')
|
|
}
|
|
// TODO: We need to also sign each request with the serverExtendedPrivateKey so that clients can verify the request was served by a server that is owned by serviceAuthenticatingExtendedPublicKey.
|
|
// Expose constructor.
|
|
module.exports = HDAuthStrategy;
|