Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Munich) with: Jens Hoppe
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Munich) with: Jens Hoppe

Constant-Time Equality Check In ColdFusion

By
Published in Comments (1)

Normally when comparing two string values in ColdFusion, I'll use either the vanilla operators, like == and !=, or the case-sensitive compare() function. But yesterday, when doing exactly this for a simple Basic Authentication check, Claude Code complained that I was opening myself up to a "Timing Attack" (in theory, though probably not in reality). To make my CFML authentication code more robust, I put a constant-time / fixed-time equality check in place.

Timing Attacks

From what I understand, most equality checks are optimized to short-circuit the comparison on the very first mismatch of characters. For example, if you have two input values that are 100-characters long, and the first character in the inputs don't match, most comparison algorithms will immediately return false without comparing the following 99-characters. After all, the two inputs are already unequal, no new information will be revealed by comparing the rest of the characters.

A malicious actor can use this short-circuiting behavior to reveal information about the contents of the input values. They can incrementally construct an input string and then compare it to the unknown private value (via HTTP requests against the application). The longer the HTTP request takes to respond, the farther along the value-comparison algorithm proceeds.

Then, one character at a time, the malicious actor keeps trying new values until the HTTP response times start to increase, revealing another "known substring" of the private value.

Within each request, there's so much noise and entropy that it's hard to imagine that this actually works. But, if a malicious actor has unbounded requests, apparently the response times - on balance - can start to leak this timing information and allow the malicious actor to narrow in on the correct input value.

Fixed-Time / Constant-Time Comparisons

Since the short-circuiting nature of simple string comparisons is the mechanism that facilitates the timing attack, the counter-measure is to use a fixed-time or constant-time comparison method on secret keys. A fixed-time comparison iterates over every byte of the input even if the operands have already proven themselves to be unequal. This way, the full space of the inputs are always explored, revealing nothing about substring matches.

To explore this concept, I created a constantTimeEquals() function. This function iterates over the input strings byte-by-byte and uses bitwise functions to compare each byte and keep a running tally. The bitwise functions are critical here because they don't do any short-circuiting that may inadvertently reveal information about the input contents:

<cfscript>

	writeDump([
		constantTimeEquals( "foobar", "foobar" ),
		constantTimeEquals( "fooBAR", "foobar" ),
		constantTimeEquals( "fooBAR", "fooBAR" ),
	]);

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I compare the equality of the two input strings using a constant time / fixed time
	* comparison. Meaning, each byte is compared even after the match has been proven to
	* be false on a substring. This prevents substring short-circuit timing from leaking
	* characteristics about the string's contents.
	*/
	public boolean function constantTimeEquals(
		required string a,
		required string b
		) {

		// Note: string length is still used to short-circuit since there's no easy way to
		// hide the fact that strings are different lengths (vis-a-vis timing attacks). A
		// malicious actor could start out with a very long string and then keep reducing
		// the length until the timing started to look constant. That *could* reveal the
		// underlying length of any secret comparison.
		if ( a.len() != b.len() ) {

			return false;

		}

		var aBytes = charsetDecode( a, "utf-8" );
		var bBytes = charsetDecode( b, "utf-8" );
		var length = arrayLen( aBytes );
		var fuzzyDiff = 0;

		for ( var i = 1 ; i <= length ; i++ ) {

			// Caution: we're explicitly using bit-wise functions here instead of logical
			// operators in order to avoid any branching / short-circuiting of the value
			// comparisons. The XOR turns byte-level differences into non-zero values.
			// Then, the OR accumulates those non-zero values over the loop leaving us
			// with a "fuzzy difference" between the two arrays. 
			fuzzyDiff = bitOr( fuzzyDiff, bitXor( aBytes[ i ], bBytes[ i ] ) );

		}

		// If any of the bits were flipped to '1', the strings are different.
		return ! fuzzyDiff;

	}

</cfscript>

If you're like me, bitwise operations aren't part of your everyday gesture. The bitXor() function looks at the bits of each input byte and maps a 1 for any bit that doesn't match on both sides of the comparison; and a 0 for any bit that does match. Which means that a non-zero result from bitXor() indicates mismatched bytes.

In that regard, the bitXor() function is somewhat like the compare() function. Both return truthy values on mismatched inputs.

The bitOr() function then accumulates the 1 bits that were reported by the bitXor() calls, pulling all 1 bits through to the next for-loop iteration.

Ultimately, if any of those 1 bits make it through to the end of the byte traversal loop, it means that some portion of the two input strings don't match. But, most importantly, we never short-circuit on that knowledge - we always inspect every byte. Information about the match / mismatch is never revealed by the execution time of the algorithm.

Want to use code from this post? Check out the license.

Reader Comments

16,219 Comments

I'll add that when I was doing some research for this post, a number of people just said to reach for Java's MessageDigest.isEqual() method. Apparently, this method has been updated to use a constant-time comparison under the hood in response to a security ticket many years ago. However, the method does not appear to be documented as such in the JavaDocs. So while folklore might say that it's good to go, I'd rather rely on the documented behavior.

Plus, isn't writing your own just so much more fun?!

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel
Managed ColdFusion hosting services provided by:
xByte Cloud Logo