Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Kurt Wiersma
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Kurt Wiersma

Experimenting With RSA-Encrypted Signature Generation And Verification In ColdFusion

By on
Tags:

CAUTION: I know very little about encryption and cryptography, so take this with a grain of salt.

Last week, I released a small project to facilitate the creation and consumption of JSON Web Tokens (JWT) in ColdFusion. The JWT standard is supposed to support several different classes of signing algorithms; however, I only released with support for the HmacSHA* key-hashing algorithms because, frankly, I didn't even know how to use the public and private keys required by the SHA* with RSA signing algorithms. But, I wanted to change that. So, my first step was just to figure out how to sign and verify messages using the *withRSA algorithms.

To create the RSA private and public keys used to sign and verify messages, respectively, I used the "openssl" command-line utility, which I got from StackOverflow:

# Create the private key with 1024 bits.
openssl genrsa -out private.pem 1024

# Extract the public key from the private key.
openssl rsa -in private.pem -out public.pem -outform PEM -pubout

NOTE: The keys are generated in PEM (Privacy Enhanced Mail) format which is a Base64 encoded DER certificate commonly used on servers.

Once I had the PEM keys for RSA encryption, I needed to figure out how to actually create the Java PublicKey and PrivateKey instances for use with the Signature object. Luckily, I found a simple tutorial by Jon Moore which demonstrated how to convert plain-text PEM keys into Java keys.

And, once I was able to convert the plain-text PEM files into Java keys, I wrapped it all up in a ColdFusion component, RSASigner.cfc. I've really been enjoying the approach of creating stateful components that hold data, like keys and algorithms, rather than trying to create static methods which require a lot of arguments. It just feels like a very clean approach.

That said, let's look at how the RSASigner.cfc can be used. In this little demo, I'm creating an instance of the RSASigner.cfc using the public and private keys generated above. Then, I'm signing and verifying a message:

<cfscript>

	// Create a new instance of our RSA Signer using the public and private keys
	// (read-in as plain-text inputs). The signer stores the public and private
	// keys internally so that we don't have to pass them around to each method.
	signer = new RSASigner(
		fileRead( expandPath( "./public.pem" ) ),
		fileRead( expandPath( "./private.pem" ) ),
		"SHA512withRSA"
	);

	// NOTE: Adding BounceCastle as a crypto API provider, otherwise, I get:
	// "Invalid RSA private key encoding." in my local development environment.
	signer.addBouncyCastleProvider();


	// I am the message to be signed.
	message = "Not malfunction Stephanie. Number 5 is alive.";

	// Sign the message (uses the PRIVATE key).
	signature = signer.sign( message );

	// Verify the signature against the message (uses the PUBLIC key).
	verified = signer.verify( message, signature );


	// Output the results:
	writeOutput( "Message: #message# <br />" );
	writeOutput( "Verified: #yesNoFormat( verified )# <br />" );

	// For additional debugging.
	// writeOutput( "Signature: #binaryEncode( signature, 'base64' )# <br />" );

</cfscript>

As you can see, the .sign() method takes the message and signs it using the internal private key. The signature, in this case, is returned as a binary object (byte array); but, it could also be encoded using Base64 or Hex. That same signature is then passed into the .verify() method where the signature is verified using the public key. And, when we run this code, we get the following page output:

Message: Not malfunction Stephanie. Number 5 is alive.
Verified: Yes

Woot! Verification a success! Now, let's take a look at the RSASigner.cfc ColdFusion component:

component
	output = false
	hint = "I provide a ColdFusion wrapper to Java hashing and verification algorithms with RSA encryption."
	{

	/**
	* I create a new RSA Signer using the given public and private keys.
	*
	* NOTE: The algorithm uses the names provided in the Java Cryptography Architecture
	* Standard Algorithm Name documentation:
	*
	* - MD2withRSA
	* - MD5withRSA
	* - SHA256withRSA
	* - SHA384withRSA
	* - SHA512withRSA
	*
	* CAUTION: The keys are assumed to be in PEM format.
	*
	* @publicKey I am the plain-text public key.
	* @privateKey I am the plain-text private key.
	* @algorithm I am the RSA-based signature algorithm being used.
	* @output false
	*/
	public any function init(
		required string publicKey,
		required string privateKey,
		required string algorithm
		) {

		setPublicKeyFromText( publicKey );
		setPrivateKeyFromText( privateKey );
		setAlgorithm( algorithm );

		return( this );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	/**
	* I add the BounceCastleProvider to the underlying crypto APIs.
	*
	* CAUTION: I don't really understand why this is [sometimes] required. But, if you
	* run into the error, "Invalid RSA private key encoding.", adding BouncyCastle may
	* solve the problem.
	*
	* This method only needs to be called once per ColdFusion application life-cycle.
	* But, it can be called multiple times without error.
	*
	* @output false
	*/
	public void function addBouncyCastleProvider() {

		createObject( "java", "java.security.Security" )
			.addProvider( createObject( "java", "org.bouncycastle.jce.provider.BouncyCastleProvider" ).init() )
		;

	}


	/**
	* I sign the given plain-text message.
	*
	* @message I am the plain-text (UTF-8) message being signed.
	* @encoding I am the encoding used when returning the signature (binary by default).
	* @output false
	*/
	public any function sign(
		required string message,
		string encoding = "binary"
		) {

		var signer = createObject( "java", "java.security.Signature" )
			.getInstance( javaCast( "string", algorithm ) )
		;

		signer.initSign( privateKey );
		signer.update( charsetDecode( message, "utf-8" ) );

		var signedBytes = signer.sign();

		// If the desired encoding is binary, just return the signed bytes.
		if ( encoding == "binary" ) {

			return( signedBytes );

		}

		// Otherwise, encode the binary data.
		return( binaryEncode( signedBytes, encoding ) );

	}


	/**
	* I verify the given signature using the given RSA-based signature algorithm.
	*
	* @message I am the plain-text (UTF-8) message being signed (and verified).
	* @signature I am the binary signature being verified.
	* @output false
	*/
	public boolean function verify(
		required string message,
		required binary signature
		) {

		var verifier = createObject( "java", "java.security.Signature" )
			.getInstance( javaCast( "string", algorithm ) )
		;

		verifier.initVerify( publicKey );
		verifier.update( charsetDecode( message, "utf-8" ) );

		return( verifier.verify( signature ) );

	}


	// ---
	// PRIVATE METHODS.
	// ---


	/**
	* I set the algorithm being used by the signer.
	*
	* NOTE: The algorithm uses the names provided in the Java Cryptography Architecture
	* Standard Algorithm Name documentation:
	*
	* - MD2withRSA
	* - MD5withRSA
	* - SHA256withRSA
	* - SHA384withRSA
	* - SHA512withRSA
	*
	* @newAlgorithm I am the RSA-based signature algorithm being set.
	* @output false
	*/
	private void function setAlgorithm( required string newAlgorithm ) {

		switch ( newAlgorithm ) {
			case "MD2withRSA":
			case "MD5withRSA":
			case "SHA256withRSA":
			case "SHA384withRSA":
			case "SHA512withRSA":

				algorithm = newAlgorithm;

			break;
			default:

				throw(
					type = "InvalidArgument",
					message = "Algorithm not supported.",
					detail = "The algorithm [#newAlgorithm#] is not currently supported."
				);

			break;
		}

	}


	/**
	* I set the public key using the plain-text public key content.
	*
	* NOTE: Keys are expected to be in PEM format.
	*
	* @publicKeyText I am the plain-text public key.
	* @output false
	*/
	private void function setPublicKeyFromText( required string publicKeyText ) {

		var publicKeySpec = createObject( "java", "java.security.spec.X509EncodedKeySpec" ).init(
			binaryDecode( stripKeyDelimiters( publicKeyText ), "base64" )
		);

		publicKey = createObject( "java", "java.security.KeyFactory" )
			.getInstance( javaCast( "string", "RSA" ) )
			.generatePublic( publicKeySpec )
		;

	}


	/**
	* I set the private key using the plain-text private key content.
	*
	* NOTE: Keys are expected to be in PEM format.
	*
	* @privateKeyText I am the plain-text private key.
	* @output false
	*/
	private void function setPrivateKeyFromText( required string privateKeyText ) {

		var privateKeySpec = createObject( "java", "java.security.spec.PKCS8EncodedKeySpec" ).init(
			binaryDecode( stripKeyDelimiters( privateKeyText ), "base64" )
		);

		privateKey = createObject( "java", "java.security.KeyFactory" )
			.getInstance( javaCast( "string", "RSA" ) )
			.generatePrivate( privateKeySpec )
		;

	}


	/**
	* I strip the plain-text key delimiters, to isolate the key content.
	*
	* @keyText I am the plain-text key input.
	* @output false
	*/
	private string function stripKeyDelimiters( required string keyText ) {

		// Strips out the leading / trailing boundaries:
		keyText = reReplace( keyText, "-----(BEGIN|END)[^\r\n]+", "", "all" );

		return( trim( keyText ) );

	}

}

As you can see, it's not terribly complicated. Mostly, it's just a wrapper around the underlying Java objects that are required to generate or verify a *withRSA signature.

Like I said above, I don't really know much about cryptography or how the heck a public key can even be used to verify a private key based signature (it's basically magic). But, this seems to work. And, now that I can sign messages using PEM keys, I should be able to build this support into my JSON Web Tokens component next.

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

Reader Comments

15,674 Comments

@All,

I think that *some* of the RSA algorithms may only be available in the Enterprise and Developer editions of ColdFusion. I am not sure how to interpret the language in the ColdFusion docs.

That said, in the Java documentation for java.security.Signature, it states:

>> Every implementation of the Java platform is required to support the
>> following standard Signature algorithms:
>>
>> SHA1withDSA
>> SHA1withRSA
>> SHA256withRSA

So, it looks like SHA256withRSA should be available on anything running on top of Java, which is what ColdFusion does.

15,674 Comments

@Henry,

That's a good question. And, to be honest, not something that I know all that much about. I can run this just fine in the local development environment. And, I believe I've also had to add BounceCastle in production before, but I think that _may have been_ in an Enterprise context. So, I am not sure if the provider is available in Standard ColdFusion.

But, I don't want to mislead you. All I can say is that I can run it locally with no additional JAR files added anywhere.

55 Comments

Huh, doing some digging and guess what CF10 comes with a super old Bouncy Castle lib at /cfusion/lib/bcprov-jdk14-139 and that's why it works!

And you're right, without adding Bouncy Castle as a provider, CF Developer & Enterprises edition can use Bsafe Crypto-J but CF Standard is out of luck.

I wonder what one would gain by loading a newer Bouncy Castle lib.

15,674 Comments

@Henry,

So, I am not sure exactly what you mean. Are you saying that the code would have to be modified to explicitly provide the RsaSignProvider? Or are you saying that is implicitly provided, which is why this may work on Standard?

55 Comments

@Ben

Btw, it is Bouncy Castle. you wrote "BounceCastle". No big deal. :)

With your provided private key in your test case, I did some investigative work:

== Without BouncyCastle, getInstance in CF Developer edition ==

getInstance() returns a RSA Signer provided by JsafeJCE version 6.0 (JsafeJCE), but RSASigner.cfc:149 throws java.security.spec.InvalidKeySpecException: Invalid RSA private key encoding on your RSASigner.cfc: line 149.

== Without BouncyCastle and with JsafeJCE removed ==
getInstance() returns a RSA Signer provided by SunRsaSign version 1.8but RSASigner.cfc:149 still throws
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence

However, once BC is added using your addBouncyCastleProvider(), your code just works with JsafeJCE or SunRsaSign. Even though BC is not the provider returned because it is added to the end of the provider priority list, somehow adding BC helped with parsing of the private key you provided in the test case.

Googled on the exception, it leads me to http://stackoverflow.com/questions/3243018/how-to-load-rsa-private-key-from-file . Is the private key you included in the unit test in pkcs8? pkcs8 should be supported out of the box, then maybe your lib does not require BC as a hard dependency.

15,674 Comments

@Henry,

That's really strange. It seems odd that merely adding the provide will cause another provider to work. Something else must be going on under the hood. But, this is beyond my understanding of keys - to be honest. As far as pkcs8 -- again, I'm not sure. I only followed a tutorial on how to generate the keys, I don't actually know too much about the keys themselves.

426 Comments

Ben. Very nice clean code. Really like the stateful component and the way you use the 'new' keyword to instantiate the component. I need to start doing more of this.

Question:

Is this the kind of thing that happens when we request a token from an OAuth endpoint?

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