Table of Contents
Abstract
There is perhaps no software engineering topic of more timely importance
than data security and integrity. Attacks are costly, whether the attack
comes from inside or out, and some attacks can expose a software company to
liability for damages. Staying on top of the most up-to-date techniques and
tools is one key to application security and integrity. Developing cross
platform applications to ensure security is a big problem which has been
encountered by a number of developers.
This tutorial is an effort to overcome such problems faced by the
developers who want to sign the data on one platform and want to verify it
on another, using popular public-key digital signature known as RSA.
Starting with a Java program to illustrate how to generate the keys, sign
and verify the data using the generated keys, both by breaking down the
functions of the application and by viewing the final execution result.
Next, the tutorial discusses how to export the public key, generated by
Java, to be used later by .NET framework to verify the data in .NET
applications. Finally, this tutorial discusses how to read the Public Key
generated by Java program in .NET (using C#) and verify the data using this
Public Key. In the end of the tutorial, different definitions of terms are
also provided to equip the beginners as well.
Section 1: Introduction
Who should read?
The main ideas behind this tutorial revolve around signing a data in Java
and then verifying it in .NET using C#. However, readers who are interested
to start learning the signing & verifying of data using Java
Cryptographic APIs and language can also use this tutorial. This tutorial
assumes that reader knows how to read and write basic Java & C# .NET
applications.
Before going through this tutorial, it is highly recommended that you
should have basic knowledge of terms for Cryptography and Security packages
defined in Java and .NET framework. For interested readers, the definitions
of terms is provided in the Glossary at the end of the tutorial. Even if you
know the terms, just go through them to re-memorize them.
Section 2: Signature generation in Java
The zip file includes the SecurityManager.java. This Java class is
responsible for signing the data and verifying the data using private and
public keys respectively. It also generates the XML Public key for .NET
framework. This class has the following responsibilities.
- Initialize Signature Parameters
- Generate Public and Private Keys
- Store/Read generated Public and Private keys
- Sign the input data
- Verify the input data
- Store the Public key in .NET compatible (XML) format
Although it is not fair to keep all the things in one class; but just to
keep it simple, everything has been kept in one class. Reader may alter the
code and may eliminate functions such as storing/reading the keys on/from
HDD to some other class.
The code has been well documented with comments, and hopefully, the
reader will not find it difficult to understand.
Public and Private Key Generation
Private Key is mandatory for digital signature generation. When you
generate the Private Key, the corresponding Public key is automatically
generated. For performance, Private / Public Keys are generated only once
and stored at physical location of the HDD. The methodology adopted in the
current code is to store the public key at some application path. However,
for web applications, key may be streamed along with data. See figure.
The following function generates the Public and Private Keys and stores
them at the specified location. This function also invokes functions to
generate the compatible .NET Public Key. It takes the size of the hash
algorithm as a parameter and generates the corresponding key.
Now, let's look at the corresponding code:
public void generateKeys(int size) {
try {
System.out.println("Generating Keys");
keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(size);
keypair = keyGen.genKeyPair();
privateKey = keypair.getPrivate();
publicKey = keypair.getPublic();
byte[] privateKeyBytes = privateKey.getEncoded();
byte[] publicKeyBytes = publicKey.getEncoded();
writeKeyBytesToFile(new
BASE64Encoder().encode(privateKeyBytes).getBytes(),
PRIVATE_KEY_FILE);
String encodedValue = new BASE64Encoder().encode(publicKeyBytes);
writeKeyBytesToFile(encodedValue.getBytes(), PUBLIC_KEY_FILE);
PrivateKey privateKey =
KeyFactory.getInstance("RSA").generatePrivate(new
PKCS8EncodedKeySpec(privateKeyBytes));
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new
X509EncodedKeySpec(publicKeyBytes));
RSAPublicKey rsaPublicKey =
(RSAPublicKey)
KeyFactory.getInstance("RSA").generatePublic(new
X509EncodedKeySpec(publicKeyBytes));
String xml = getRSAPublicKeyAsXMLString(rsaPublicKey);
writeKeyBytesToFile(xml.getBytes(), DOT_NET_PUBLIC_KEY_FILE);
}
catch (java.security.NoSuchAlgorithmException e) {
System.out.println(
"No such algorithm. Please check the JDK version."+e.getCause());
}
catch (java.security.spec.InvalidKeySpecException ik) {
System.out.println(
"Invalid Key Specs. Not valid Key files."+ ik.getCause());
}
catch (UnsupportedEncodingException ex) {
System.out.println(ex);
}
catch (ParserConfigurationException ex) {
System.out.println(ex);
}
catch (TransformerException ex) {
System.out.println(ex);
}
catch (IOException ioe) {
System.out.println("Files not found on specified path. "+
ioe.getCause());
}
}
Once the keys are generated, they can be stored at specific path in a
simple file to avoid re-generation of Private and Public keys. It increases
the performance. There may be a requirement to generate new keys each time
for each request. If you want to generate the keys each time then invoke
this function each time in main function or in your application before
signing the data.
Public and Private Key initialization from stored
files
Following two functions read the public and private keys. Whenever you
have to sign the data, you only require a private key. Therefore, I have
provided an additional function to initialize only the private key. The
basic work flow of this function is described in the figure.
Let's look at the code now. As by name, it is clear the
initializePrivateKey
only initializes the private key, but if
you wish to initialize both private and public keys, invoke the other
function named as initializeKeys
.
private void initializePrivateKey() {
try {
BASE64Decoder decoder = new BASE64Decoder();
byte[] privateKeyBytes = decoder.decodeBuffer(new
String(readPrivateKeyFile()));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
EncodedKeySpec privateKeySpec = new
PKCS8EncodedKeySpec(privateKeyBytes);
privateKey = keyFactory.generatePrivate(privateKeySpec);
}
catch (IOException io) {
System.out.println("Private Key File Not found."+
io.getCause());
}
catch (InvalidKeySpecException e) {
System.out.println("Invalid Key Specs. Not valid Key files."
+ e.getCause());
}
catch (NoSuchAlgorithmException e) {
System.out.println("There is no such algorithm." +
" Please check the JDK ver."+ e.getCause());
}
}
private void initializeKeys() {
try {
BASE64Decoder decoder = new BASE64Decoder();
byte[] privateKeyBytes = decoder.decodeBuffer(new
String(readPrivateKeyFile()));
byte[] publicKeyBytes = decoder.decodeBuffer(new
String(readPublicKeyFile()));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
EncodedKeySpec privateKeySpec =
new PKCS8EncodedKeySpec(privateKeyBytes);
privateKey = keyFactory.generatePrivate(privateKeySpec);
EncodedKeySpec publicKeySpec =
new X509EncodedKeySpec(publicKeyBytes);
publicKey = keyFactory.generatePublic(publicKeySpec);
}
catch (IOException io) {
System.out.println("Public/ Private Key File Not found."
+ io.getCause());
}
catch (InvalidKeySpecException e) {
System.out.println("Invalid Key Specs. Not valid Key files."
+ e.getCause());
}
catch (NoSuchAlgorithmException e) {
System.out.println("There is no such algorithm." +
" Please check the JDK ver."+ e.getCause());
}
}
Signing the Data
As it is evident from the function name, that function is responsible for
signing the data sent as bytes. Prior to this function invocation, the
private key initialization is must. Either read the existing private key or
generate a new private key. See figure.
public byte[] signData(byte[] toBeSigned) {
if (privateKey == null) {
initializePrivateKey();
}
try {
Signature rsa = Signature.getInstance("SHA1withRSA");
rsa.initSign(privateKey);
rsa.update(toBeSigned);
return rsa.sign();
}
catch (NoSuchAlgorithmException ex) {
System.out.println(ex);
}
catch (InvalidKeyException in) {
System.out.println("Invalid Key file." +
"Please chekc the key file path"+ in.getCause());
}
catch (SignatureException se) {
System.out.println(se);
}
return null;
}
Verify data and Signature
Verifying the data is fairly simple. The following function is
responsible for verifying the signature for corresponding data, sent as
bytes. The signature is verified with the Public Key. See figure.
public boolean verifySignature(byte[] signature, byte[] data) {
try {
initializeKeys();
sign.initVerify(publicKey);
sign.update(data);
return sign.verify(signature);
}
catch (SignatureException e) {
e.printStackTrace();
}
catch (InvalidKeyException e) {
}
return false;
}
Section 3: Generating .NET compatible Public Key
.NET RSA Public Key XML
.NET RSA Public Key contains Modulus and Exponent which can be extracted
from the Java Public key. Before we look into the Java code details, let's
look at the XML which can be transformed into the RSAParameters structure.
The Modulus
represents the Modulus parameter for the RSA
algorithm while Exponent
represents the Exponent parameter for
the RSA Algorithm. The RSA algorithm class in .NET is available under the
namespace System.Security.Cryptography.RSA
.
The .NET RSA Public XML key structure is given below.
="1.0" ="UTF-8"
<RSAKeyValue>
<Modulus>uKI+0wG6eILUPRNf6ImqRdez/nLxsV0LHGsuvYR0LDVrXz8Y7sYSlpAkn1HpJI8US8Sx5bJzvBib
vKv0pAa7UQ==
</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
Generating .NET XML Public Key from Java
In this section, we will discuss the methodology adopted to export the
Java public key class as .NET compatible public key. The exported XML key
will be later loaded in our .NET application and we will verify the
signature by using this key.
Let's look at the code.
Recall from our Key Generation process that we invoked some functions to
generate the Public key in XML format.
Instance of RSAPublicKey class with X509 encoding key specs
RSAPublicKey rsaPublicKey = (RSAPublicKey)
KeyFactory.getInstance("RSA").
generatePublic(new X509EncodedKeySpec(publicKeyBytes));
String xml = getRSAPublicKeyAsXMLString(rsaPublicKey);
writeKeyBytesToFile(xml.getBytes(), DOT_NET_PUBLIC_KEY_FILE);
If you see the first line, X509EncodedKeySpec
has been used.
This class represents the ASN.1 encoding of a public key, encoded according
to the ASN.1 type SubjectPublicKeyInfo
. The
SubjectPublicKeyInfo
syntax is defined in the X.509 standard as
follows:
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
Now, look at the function named as
getRSAPublicKeyAsXMLString
:
private String getRSAPublicKeyAsXMLString(RSAPublicKey key) throws
UnsupportedEncodingException,
ParserConfigurationException,
TransformerException {
Document xml = getRSAPublicKeyAsXML(key);
Transformer transformer =
TransformerFactory.newInstance().newTransformer();
StringWriter sw = new StringWriter();
transformer.transform(new DOMSource(xml), new StreamResult(sw));
return sw.getBuffer().toString();
}
private Document getRSAPublicKeyAsXML(RSAPublicKey key) throws
ParserConfigurationException,
UnsupportedEncodingException {
Document result =
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element rsaKeyValue = result.createElement("RSAKeyValue");
result.appendChild(rsaKeyValue);
Element modulus = result.createElement("Modulus");
rsaKeyValue.appendChild(modulus);
byte[] modulusBytes = key.getModulus().toByteArray();
modulusBytes = stripLeadingZeros(modulusBytes);
modulus.appendChild(result.createTextNode(
new String(new sun.misc.BASE64Encoder().encode(modulusBytes))));
Element exponent = result.createElement("Exponent");
rsaKeyValue.appendChild(exponent);
byte[] exponentBytes = key.getPublicExponent().toByteArray();
exponent.appendChild(result.createTextNode(
new String(new sun.misc.BASE64Encoder().encode(exponentBytes))));
return result;
}
The above mentioned functions allow us to generate the XML file which
contains the Public Key Information. We store this file on the hard disk or
stream it over the network.
Section 4: Reading Public Key and verifying the Data in
.NET
In this section, we will read the Public key which was generated in the
previous section by the Java program.
Initialization
In the constructor, initialize RSAParameters
class and
RSACryptoServiceProvider
.
PUBLIC_KEY="c:\\netpublic.key";
RSAKeyInfo = new RSAParameters();
RSA = new RSACryptoServiceProvider();
Reading Public Key in .NET
Following function reads the file from the specific path on HDD. One may
use network stream to read the XML file. Parse the modulus data and exponent
data and assign them to variables. Convert the strings from Base64 and
assign them to RSAParameters.Modulus
and
RSAParameters.Exponent
. ImportParameters
function
of RSACryptoServiceProvider
is used to load the parameters for
RSA algorithm.
private void readKey()
{
try
{
XmlTextReader reader = new XmlTextReader(PUBLIC_KEY);
while(reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if(reader.Name=="Modulus")
{
reader.Read();
modStr= reader.Value;
}
else if(reader.Name=="Exponent")
{
reader.Read();
expStr= reader.Value;
}
}
}
if(modStr.Equals("") ||expStr.Equals(""))
{
throw new Exception("Invalid public key");
}
RSAKeyInfo.Modulus = Convert.FromBase64String(modStr);
RSAKeyInfo.Exponent = Convert.FromBase64String(expStr);
RSA.ImportParameters(RSAKeyInfo);
}
catch(Exception e)
{
throw new Exception("Invalid Public Key.");
}
}
Verifying the Signature in .NET
The signature can be verified once the Modulus and Exponent have been
parsed from the XML and loaded in RSACryptoServiceProvider
.
verifySignature
method is invoked to verify the signature for
given data. It verifies the specified signature data by comparing it to the
signature computed for the specified data. See figure.
public bool verifySignature(byte[]
signature , string signedData)
{
byte[] hash = Convert.FromBase64String(signedData);
try
{
if(RSA.VerifyData(hash,"SHA1",signature))
{
return true;
}
else
{
return false;
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
return false;
}
}
References
Acknowledgements
Special thanks to:
- Dr. Iman Gholampur
- Philip Ross
- Mitch Gallant