Credential Registration for Cryptographic Authentication with Node.js and MongoDB

This is part 2 of a series of posts describing a proof-of-concept web app that implements cryptographic authentication using Node.js, Express, Handlebars, MongoDB and Mongoose. All parts are now available. Part 1 describes the login process. This Part 2 describes the registration process. Part 3 describes login session maintenance. Part 4 is concerned with random bit generation.

Update. The name of the constant securityStrength has been changed to rbgSecurityStrength as noted in the last post of the series and reflected in the snippets below.

Part 1 of this series described the login process of a proof-of-concept Node.js application that implements cryptographic authentication using a MongoDB database back-end. The app, called app-mongodb.js, can be found in a zip file downloadable from the cryptographic authentication page, where it is bundled together with a simpler app that has the same functionality and the same front-end but emulates the database using JavaScript objects, provided for comparison.

This post describes the registration process of app-mongodb.js. The app has a registration page reachable from a link found under a top-of-page login form in the public pages of the app. The registration page has a form where the user enters a username, a first name and a last name, but no password. The first and last names are representative of any info that the user may be asked to provide in a full-fledged application.

The registration process of app-mongodb.js has a structure similar to that of the login process described in Part 1. The browser sends an HTTP POST request to the /register-username endpoint of the server, conveying the username, first name and last name. The server creates a user record, called a “user document” in MongoDB terminology, and responds with a JavaScript POST redirection. The JavaScript POST redirection consists of downloading a script that generates a key pair, signs a server challenge with the private key, and sends the public key and the signature to the /register-public-key endpoint in a second HTTP POST request. The server cryptographically validates the public key, verifies the signature, and adds the public key to the user document.

The following code snippet shows how the server processes the first HTTP POST request, received at the /register-username endpoint.

app.post('/register-username',function(req,res) {
	var username = req.body.username;
	var firstname = req.body.firstname;
	var lastname = req.body.lastname;
	if (
		username.search(/^[A-Za-z0-9]+$/) == -1 ||
		firstname.search(/^[A-Za-z]+$/) == -1 ||
		lastname.search(/^[A-Za-z]+$/) == -1
	) {
		res.redirect(303, "/registration-failure.html");
		return;
	}
	var entropyHex = pjclBitArray2Hex(pjclRBGGen(rbgStateObject,rbgSecurityStrength,rbgSecurityStrength));
	var challengeHex = pjclBitArray2Hex(pjclRBGGen(rbgStateObject,rbgSecurityStrength,rbgSecurityStrength));
	var user = new User({
		username: username,
		name: {
			firstname: firstname,
			lastname: lastname
		},
		regChallenge: challengeHex,
		regTimeStamp: (new Date()).getTime()
	});
	user.save(function(err) {
		if (err) {
			res.redirect(303, "/username-taken.html");
		}
		else {
			res.render("reg-redir.handlebars", {
				entropyHex: entropyHex,
				challengeHex: challengeHex,
				username: username
			});
		}
	});
});

At lines 2–12 the server obtains and validates the username, first-name and last-name inputs, redirecting to a registration-failure error page if they contain unexpected characters. For simplicity, the proof-of-concept app-mongodb.js restricts the range of characters allowed in the first and last names to ASCII letters. A much wider range of Unicode characters should of course be allowed in a full-fledged web app.

Client-side validation of the inputs to the registration form is performed on the browser by the checkRegistration function in views/register.handlebars. Additional server-side validation is performed as a defense against cross-side scripting (XSS). A further, redundant defense against XSS is provided by the character escaping performed by Handlebars in view variables surrounded by double, rather than triple, curly brackets. Handlebars is an extension of the Mustache templating engine, and the escaping performed by Mustache can be seen by looking at the function escapeHTML and the variable entityMap found here.

Lines 13–14 assign random hex strings to two variables, entropyHex and challengeHex. The former is used to convey server-side entropy to the browser, where it will be combined with browser entropy for client-side random bit generation. Client and server-side random bit generation will be discussed in more detail in another post of the series dedicated to that topic. The latter is the server challenge to be sent to the browser and signed by the browser after the key pair has been generated.

Lines 16–24 create an instance “user” of the Mongoose modelUser” with properties username, name, regChallenge and regTimeStamp. Saving that user model instance to the database will result in the creation of a user document, the properties of the model instance becoming fields of the user document.

To illustrate the fact that MongoDB supports documents with fields that are composite objects, the value of the name property is an object with two properties firstname and lastname. MongoDB also has a Date type for field values, but using it in app-mongodb.js would create unnecessary complications. We avoid those complications by using the number of milliseconds since the Unix Epoch, produced in JavaScript by the expression “(new Date()).getTime()”, as the value of the property regTimeStamp, which is used to limit the time taken by the JavaScript POST redirection.

The server challenge used in the registration process is assigned to the property regChallenge of the user model instance, and will be stored in the regChallenge field of the user document when the model instance is saved. Notice how this is different from the field loginChallenge used to store the server challenge used in the login process as we saw in Part 1. This eliminates any danger that an attacker may be able to use a login challenge in a registration replay attack.

An attempt to save the user model instance is made at lines 25–36. The Mongoose user model is associated with a Mongoose “userSchema”, which specifies that the username field must be unique among user documents. There are no schemas in MongoDB, which is a nosql database management system, but a Mongoose model superimposes a schema on a database collection, and uniqueness of a field specified by the schema is enforced in the database by means of a MongoDB “unique index”. The save operation will fail if there is already a user document with the same username in the “users” database collection, which is implicitly associated by Mongoose with the user model by converting the name “User” of the model to lower case and “pluralizing” it.

After the save operation has been processed the callback defined at lines 24–36 is passed an error object as an argument. To keep things simple, if err is not null the callback does not check to see what kind of error object has been returned. It assumes that the cause of the error is an existing user document with the same username and redirects to a username-taken error page. If it is null, the callback responds to the request received at the /register-username endpoint with the JavaScript POST redirection, by rendering the view reg-redir.handlebars, shown in the next snippet.

<script src="/pjcl.js"></script>
<script src="/browser-entropy.js"></script>
<script src="/domain-parameters.js"></script>
<script>
var serverEntropy = "{{entropyHex}}";
var challenge = "{{challengeHex}}";
var username = "{{username}}";
// the following function is derived from:
// https://stackoverflow.com/questions/133925/javascript-post-request-like-a-form-submit
function post(path, params) {
    var form = document.createElement("form");
    form.setAttribute("method", "post");
    form.setAttribute("action", path);
    for(var key in params) {
        if(params.hasOwnProperty(key)) {
            var hiddenField = document.createElement("input");
            hiddenField.setAttribute("type", "hidden");
            hiddenField.setAttribute("name", key);
            hiddenField.setAttribute("value", params[key]);
            form.appendChild(hiddenField);
        }
    }
    document.body.appendChild(form);
    // form.submit();
	window.onload = function () { form.submit(); }; // to bypass the Firefox bug
}
var entropy = pjclBrowserEntropy128Bits().concat(pjclHex2BitArray(serverEntropy));
pjclRBG128InstantiateOrReseed(localStorage,entropy);
var p = pjclHex2BigInt(pHex);
var q = pjclHex2BigInt(qHex);
var g = pjclHex2BigInt(gHex);
var keyPair = pjclFFCGenKeyPair(localStorage,p,q,g);
var privKeyHex = pjclBigInt2Hex(keyPair.x);
var pubKeyHex = pjclBigInt2Hex(keyPair.y);
localStorage.privKey = privKeyHex;
localStorage.pubKey = pubKeyHex;
var msg = pjclHex2BitArray(challenge);
var signature = pjclDSASignMsg(localStorage,p,q,g,keyPair.x,msg);
var sigRHex= pjclBigInt2Hex(signature.r);
var sigSHex= pjclBigInt2Hex(signature.s);
post("/register-public-key", {username: username, publicKey: pubKeyHex, signatureR: sigRHex, signatureS: sigSHex});
</script>

The view reg-redir.handlebars is similar to the view login-redir.handlebars discussed in Part 1.

Lines 1–3, like the first three lines of login-redir.handlebars, source the static files pjcl.js, browser-entropy.js and domain-parameters.js.

While describing the login process in Part 1, all I needed to say about domain-parameters.js is that it contains the finite field cryptography (FFC) domain parameters (p,q,g) used by the application for computing and verifying DSA signatures. Now I need to go into more detail.

As discussed in the preamble of Section 23 of the PJCL Documentation, NIST uses FFC to refer to public-key cryptographic primitives, including DSA and Diffie-Hellman (DH), that rely on the difficulty of computing discrete logarithms in the multiplicative group of the field of integers modulo p, where p is a prime number such that p – 1 is divisible by a large prime q. An FFC primitive uses domain parameters (p,q,g), where g is a generator of the unique subgroup of order q of the multiplicative group.

FFC primitives have nominal security strengths that depend on the bit lengths L and N of p and q. The current version of PJCL can generate domain parameters with lengths (L, N) = (2048, 256) and (L, N) = (3072, 256), which provide nominal security strengths of 112 and 128 bits respectively according to Table 2 of SP 800-57 Part 1 Revision 4. The primes p and q in domain-parameters.js have bit lengths 2048 and 256, and thus provide a nominal security strength of 112 bits.

You can generate the domain parameters for your web app using the functions pjclFFCGenPQG_2048_256 or pjclFFCGenPQG_3072_256 of the PJCL library, which generate the primes p and q as specified in Section A.1.1.2 of FIPS 186-4, and the generator g as specified in Section A.2.3 of the standard. You can also use the facility for measuring DSA performance that comes with the library in the DSAPerfTesting directory of the downloadable zip archive. That facility lets you generate random domain parameters with 112 or 128 bits of security strength for the purpose of measuring the performance of key pair generation, signature, and verification. Parameters can be generated with one click in Firefox, Chrome or Edge, with computations carried out in the background by a web worker. The parameters in domain-parameters.js were generated that way before being set as defaults for the facility.

Lines 5–7 assign the values of the view variables entropyHex, challengeHex and username to corresponding script variables, thus conveying server entropy and the registration challenge to the browser along with the username.

Lines 9–27 define the same form submission function post used in login-redir.handlebars, borrowed from this Stack Overflow discussion but modified to cope with a Firefox bug by postponing submission to the window.onload event handler. (We could have avoided the duplication of the function definition by using a partial view or sourcing the definition from a static JavaScript file.)

Lines 29–30 initialize (“instantiate” in NIST terminology) or reseed a client-side random bit generator (RBG) in browser local storage.

The RBG functionality provided by the PJCL library is based on the Hash_DRBG mechanism specified in SP 800-90A Revision 1. The security strength of an RBG based on the Hash_DRBG mechanism is limited by two factors: the hash function that it uses, and the entropy with which it is initialized.

The current version of PJCL provides two RBG flavors, which use hash functions SHA-256 and SHA-384, and thereby allow for 128 and 192 bits of security strength respectively according to Table 3 of SP 800-57 Part 1 Revision 4. The function pjclRBG128InstantiateOrReseed of line 30 uses SHA-256 and thus provides 128 bits of security strength if enough initial entropy is provided.

The initial entropy limits the security strength of an RBG, specified in bits, to be equal to the entropy. Line 29 assembles the initial entropy of the client-side RBG by concatenating 128 bits supplied by the browser and 128 bits obtained from the server. The browser bits are obtained from crypto.getRandomValues by the function pjclBrowserEntropy128Bits defined in browser-entropy.js. The function crypto.getRandomValues, part of the W3C Web Crypto API, is not guaranteed to provide full entropy. But the server bits come from /dev/random rather than /dev/urandom in a manner that does provide full entropy, as will be discussed in the post of this series dedicated to random bit generation. Thus the initial entropy of the client-side RBG is at least 128.

From the choice of hash function and the amount of initial entropy it follows that the client-side RBG of app-mongodb.js has a security strength of 128 bits.

At lines 31–34 the hex-encoded domain parameters are converted to big integers and used to generate the DSA key pair that will be used for cryptographic authentication. Key pair generation is performed by the PJCL library function pjclFFCGenKeyPair using the RBG in found in the object passed as first argument, which here is localStorage. The client-side RBG found in localStorage has a security strength of 128 bits, but the domain parameters sourced from domain-parameters.js have a security strength of only 112 bits, so the key pair has 112 bits of security strength. Using domain parameters (p, q) with bitlengths (3072, 256) rather than (2048, 256) would result in a key pair with 128 bits of security strength.

While it is OK for the domain parameters to provide less security strength than the RBG, the nominal security strength determined by the domain parameters should not be lessened by the RBG, because that could mislead code reviewers into thinking that the overall security strength is greater than it is. Consequently, in the argument-checking mode of the PJCL library (provided by a separate file pjcl-withArgChecking.js for use during app development) pjclFFCGenKeyPair throws an exception if the RBG is less secure than the domain parameters.

Lines 35–38 hex-encode the private and public components (x, y) of the key pair and save them to localStorage. What if there is already a key pair in localStorage? For simplicity, this proof-of-concept app silently overwrites the existing key pair. A full-fledged app would warn that replacing the key pair could make an existing account inaccessible, and ask for confirmation.

Lines 39–42 convert the hex-encoded server challenge to a bit array, sign the resulting msg using the PJCL library function pjclDSASignMsg, and hex-encode the (r, s) components of the signature.

The function pjclDSASignMsg hashes the message before signing it, using a hash function of the SHA-2 family with a security strength not less than the nominal security strength determined by the domain parameters. If no such hash function is available in the library, pjclDSASignMsg throws an exception both with and without argument-ckecking. If multiple hash functions with enough security strength are available, the one with shortest output is chosen. (The PJCL library also provides a function pjclDSASignHash that takes as an argument the hash of the message rather than the message itself, allowing for an explicit choice of hash function.) Here, SHA-224 would provide enough security strength according to Table 3 of SP 800-57 Part 1 Revision 4, but it is not included in the current version of the library because it is not any faster than SHA-256. So SHA-256 is chosen.

DSA is a randomized signature scheme, which uses a random per-message secret. The function pjclDSASignMsg takes an ordinary or storage object as its first argument, where it expects to find the state of an RBG of sufficient strength. With argument checking, it throws an exception if that’s not the case. Here, at line 40, the first argument is localStorage, which contains the client-side RBG with 128 bits of security strength.

Finally, at line 43 the post function is used to send the username, the public key and the two components of the signature to the /register-public-key endpoint of the server.

The following snippet shows how the server processes a POST request received at the /register-public-key endpoint.

app.post('/register-public-key',function(req,res) {
	var username = req.body.username;
	var pubKeyHex = req.body.publicKey;
	var sigRHex = req.body.signatureR;
	var sigSHex = req.body.signatureS;
	if (
		username.search(/^[A-Za-z0-9]+$/) == -1 ||
		pubKeyHex.search(/^[A-Za-z0-9]+$/) == -1 ||
		sigRHex.search(/^[A-Fa-f0-9]+$/) == -1 ||
		sigSHex.search(/^[A-Fa-f0-9]+$/) == -1
	) {
		res.redirect(303, "/registration-failure.html");
		return;
	}
	User.findOne({username: username}).select('regTimeStamp regChallenge').exec(function(err, user) {
		if (err) throw new Error(err);
		if (!user) {
			res.redirect(303, "/registration-failure.html");
			return;
		}
		var now = (new Date()).getTime();
		if (!user.regTimeStamp || (now - user.regTimeStamp > regRedirTimeout)) {
			res.redirect(303, "/registration-failure.html");
			return;
		}
		var challengeHex = user.regChallenge;
		if (!challengeHex) {
			res.redirect(303, "/registration-failure.html");
			return;
		}
		var challengeBitArray = pjclHex2BitArray(challengeHex);
		var p = pjclHex2BigInt(pHex);
		var q = pjclHex2BigInt(qHex);
		var g = pjclHex2BigInt(gHex);
		var y = pjclHex2BigInt(pubKeyHex);
		if (!pjclFFCValidatePublicKey(p,q,g,y)) {
			res.redirect(303, "/registration-failure.html");
			return;
		}
		var r = pjclHex2BigInt(sigRHex);
		var s = pjclHex2BigInt(sigSHex);
		if (!pjclDSAVerifyMsg(p,q,g,y,challengeBitArray,r,s)) {
			res.redirect(303, "/registration-failure.html");
			return;
		}
		user.publicKey = pubKeyHex;
		user.regChallenge = "";
		user.save(function (err) {
			if (err) throw new Error(err);
			var sessionId = pjclBitArray2Hex(pjclRBGGen(rbgStateObject,rbgSecurityStrength,rbgSecurityStrength));
			var session = new Session({
				sessionId: sessionId,
				username: username,
				timeStamp: new Date().getTime()
			});
			session.save(function (err) {
				if (err) throw new Error(err);
				res.cookie('session', sessionId, {httpOnly: true, secure: true});
				res.redirect(303, "/private-page-1.html");
			});
		});
	});
});

Like requests that target the /check-username or /verify-signature endpoints, discussed in Part 1, and requests that target the /register-username endpoint, discussed above, requests that target /register-public-key are processed by a cascade of callbacks with nested definitions starting at lines 1, 15 and 48 of the snippet.

At lines 2–14 the line-1 callback obtains and validates the inputs submitted by the JavaScript POST redirection of the previous snippet, redirecting the browser to the registration-failure page if validation fails.

At line 15, the line-1 callback issues a query for a user document whose username field matches the username submitted as an input to the endpoint. Then execution of the callback terminates after associating the line-15 callback with the query.

The line-15 callback is called when the query has been processed, and may be passed an error object as its first argument or a user model instance as its second argument.

At line 16, if an error object has been passed, the line-15 callback throws an exception. The exception is caught by Express and passed to the very last middleware in app-mongodb.js (not shown in the snippet), which is identified as error-handling middleware by the fact that it takes four arguments.

At lines 17–20 the line-15 callback redirects to the registration-failure page if no user document has been found, and at lines 21–25 if the registration timestamp in the user document is older than a timeout.

At lines 26–31 the line-15 callback redirects to the registration-failure page if there is no registration challenge in the user document. Otherwise it converts the hex-encoded registration challenge to a bit array.

At lines 32–34 the hex-encoded domain parameters pHex, qHex and gHex are converted to big integers.

I should have explained in Part 1 that the variables pHex, qHex and gHex are assigned their values in the file domain-parameter.js, which is used both as a static file sourced by the client-side scripts in reg-redir.handlebars and login-redir.handlebars, and as a Node.js module required by app-mongodb.js. Although domain-parameter.js is used as a required module, no module.exports properties are defined in the file. The variables pHex, qHex and gHex are global variables, and they can be used without a prefix after requiring the module because they are not declared with the var keyword inside the module.

The file pjcl.js that contains the PJCL library is also used as a static file downloaded to the client, and a module required by server code. The library functions can be used in the server without a prefix because they are named by assigning their definitions to global variables not declared with the var keyword, as discussed here.

At lines 35–39, the public key submitted as an input is converted to a big integer y then validated, with redirection to the registration-failure page if validation fails. The function pjclFFCValidatePublicKey, in accordance with Section 5.6.2.3.1 of SP 800-56A Revision 3, checks that y is in the subgroup of order q of the multiplicative group of integers modulo p, after checking that it is in the range 2 ≤ yp – 2. The purpose of pjclFFCValidatePublicKey is to prevent the Lim-and-Lee attack against DH, where the attacker presents a public key that is in a small subgroup. Here the public key is not used for DH, so the validation may not be necessary. However, since a validation routine is available, and it is generally good practice to verify that inputs are as expected, in this case that the public key input to the /register-public-key endpoint is in the subgroup of order q, we have included the validation. Validation does have a cost, viz. one exponentiation modulo p. If you believe that is completely unnecessary, please say so and explain your thinking in a comment.

At lines 40–45 the line-15 callback verifies the signature on the registration challenge, again with redirection to the registration-failure page if verification fails.

At lines 46–48 the line-15 callback updates the user model instance passed as the second argument, and propagates the updates to the user document by saving the model instance. The updates consist of adding the hex-encoded public key, thus registering it, and removing the registration challenge as a defense against registration replay attacks.

After the save operation the line-48 callback throws an exception if it has been passed an error object. Otherwise it creates a login session and sets a cookie with the session ID, so that the user is logged in after successful registration. Login session maintenance will be discussed in another post of this series.

Leave a Reply

Your email address will not be published.