How digital signature works

Asymmetric cryptography works with two keys: one for encryption - and one for decryption. You cannot deduce one from the other. When you make the decryption key public and keep the encryption key private, you may use the scheme for digital signature. When you publish the encryption key instead, you're performing "traditional" public cryptography (like in PGP: Pritty Good Privacy), where people may encrypt a message for you wihtout having the ability do decrypt it themselves.

Digital signature use public key crytography. Keys involved are: A message digest is basicly a irreversable hashcode generated from the message. Encrypting a digest with the private key using asymmetric cryptography creates the digital signature of the person or entity known to own the private key. Anybody using the corresponding public key may decrypt the signature, get the message digest and verify that it indeed corresponds to the original message. This implies strongly that the original message was encrypted with the private key corresponding to the public key. Since only the owner holdes the private, it can be assumed that the message was signed by the owner of the private key.

The X.509 standard describes in details actually how to handle message exchange using digital signature. Make sure you understand it.

Basicly the procedure is:

Step 1: creating a digital signature using J2EE API

Creating a digital signature requires generation of a private key (key pair). This is done in Java, using the KeyPairGenerator class. calling the getInstance method on the KeyPairGenerator class creates the key pair generator. It is possible to use a number of different signature algorithms for the generator (Sun Microsystems actually provides a Digital Signature Algorithm, or DSA).

Code summary

// create a key pair generator
KeyPairGenerator keyInstance = KeyPairGenerator.getInstance("signaturealgorithm");

// init keypair generator
SecureRandom random = SecureRandom.getInstance("algorithm", "provider");
keyInstance.initialize(sizeinbits, randomsource);

// store key pair
KeyPair pair = keyInstance.generateKeyPair();
PrivateKey private = pair.getPrivate();
PublicKey public = pair.getPublic();

The java class java.security.Signature has methods to create and verify a signature. Using the factory pattern, a Signature object is created using static method getInstance(). Initialized using .initSign() with the private key as an argument. Method .sign() produces the signature bytes. Correspondingly, verification is performed by first initializing with public key as argument to .initVerify() and checking with .verify() whether a particular signature has been created using the corresponding private key or not.

Step 2: creating a message digest for the message

This part is handled "behind the scene" for you when using the J2EE API.

Basicly message digest is really just a hash-code of the message. It is possible to generate this hashcode using various schemes ("SHA", MD5"). Encrypting a message digest and not the full message has advantages:

Step 3: signing the data

This part is also handled "behind the scene" when using J2EE API. The J2SE bundled providers supports signature algorithms: You can basicly see "SHA1withRSA" done without the J2EE api in example 2.

Step 4: transmitting

Only the actual message should be transmitted through an encrypted channel. When only digital signature validation is the issue, an encrypted channel is not required at all (in principle).

But the public key should be transmitted through a "secure channel" - meaning you should be sure that it actually originates from the source you're intending to verify. This issue is sometimes overlooked.

Just because somebody is making up a new key pair impersonating an identity - it doesn't imply true identity just because the fake keyset matches each other. Normally, this is handled by checking the public key with a CA authority.

Step 5: receiving and verifying

The receiver obtains: Using the java.security API, the receiver can verify the signature. It is necessary to import the encoded public key bytes and convert them to PublicKey. PublicKey is required for the initializing method Signature.initVerify. Having obtained the encoded public key bytes, the KeyFactory class is used to instantiate a public key. Using a key specification and a KeyFactory object to do the conversion - the KeyFactory object generates a PublicKey from the key specification.

When not using the J2EE API methods, it is required to calculate the message digest yourself and encrypting it. This is illustrated in example 2.

It is necessary to create a Signature object using same algorithm used to generate the signature. When the Signature object has processed all message data, the signature may be verified. Using method .verify(), the signature object is asked to check the data processed with a specified signature (in a byte array). If returning true, the alleged signature matches the actual signature generated by the corresponding private key.


Example 1: handling signature creation and verification using java.security.Siganture API
DigitalSignatureSimpleEncryption.java

package dk.cryptography;

import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.PrivateKey;
import java.security.Signature;
import java.io.FileInputStream;
import java.io.File;

public class DigitalSignatureSimpleEncryption {
  public static void main(String[] unused) {
    try {
      // Generate a key-pair
      KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
      kpg.initialize(512); // keysize is 512 bits
      KeyPair kp = kpg.generateKeyPair();
      PublicKey pubk = kp.getPublic();
      PrivateKey prvk = kp.getPrivate();
      String dataFileName = "DigitalSignatureSimpleEncryption.java";
      byte[] signaturebytes = sign(dataFileName, prvk, "SHAwithDSA");
      System.out.println("Signature(hex, length="+signaturebytes.length+"):" +
        byteArray2Hex(signaturebytes));
      boolean result = verify(dataFileName, pubk, "SHAwithDSA", signaturebytes);
      System.out.println("\nSignature Verification Result = " + result);
    }
    catch(Exception ex) {
       ex.printStackTrace();
    }
  }
  private static byte[] sign(String datafile, PrivateKey prvKey,
      String sigAlg) throws Exception {
    Signature sig = Signature.getInstance(sigAlg);
    sig.initSign(prvKey);
    File f;
    FileInputStream fis = new FileInputStream(f=new File(datafile));
    byte[] dataBytes = new byte[(int)f.length()]; //small file - read all
    int nread = fis.read(dataBytes); //hopefully nread==fis.length()
    fis.close();
    sig.update(dataBytes, 0, nread);
    return sig.sign();
  }
  private static boolean verify(String datafile, PublicKey pubKey,
      String sigAlg, byte[] sigbytes) throws Exception {
    Signature sig = Signature.getInstance(sigAlg);
    sig.initVerify(pubKey);
    File f;
    FileInputStream fis = new FileInputStream(f=new File(datafile));
    byte[] dataBytes = new byte[(int)f.length()]; //small file - read all
    int nread = fis.read(dataBytes); //hopefully nread==fis.length()
    sig.update(dataBytes, 0, nread);
    return sig.verify(sigbytes);
  }
  private static String byteArray2Hex(byte[] byteArray){
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < byteArray.length; i++){
            String hex = Integer.toHexString(255 & (int)byteArray[i]);
            sb.append( ((i%8)==0 ? "\n " : " ") 
                        + (hex.length()<2 ? ("0"+hex) : hex));
        }
        return sb.toString();
  }
}


Basicly you may also calculate the digest yourself - and encrypt it with an algorithm. This is not recommended, since it goes outside the J2EE security framework support. But clearly, this is a possilbity.

Consider a SHA digest combined with RSA encryption...

Example 2: handling simple signature creation and verification - yourself!
SignatureDigest.java

package dk.cryptography;

import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;

public class SignatureDigest {

  public static void main(String[] args) {
    String messageText = "To whom it may concern, ...";
    try {
      MessageDigest theDigest = MessageDigest.getInstance("SHA"); //SHA or MD5
      theDigest.update(messageText.getBytes()); //update digest w/message
      byte[] digestBytes = theDigest.digest(); //compute digest

      //for demo, generate private/public keys      
      KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
      kpg.initialize(512); // keysize is 512 bits
      KeyPair kp = kpg.generateKeyPair();
      RSAPublicKey pubk = (RSAPublicKey)kp.getPublic();
      RSAPrivateKey prvk = (RSAPrivateKey)kp.getPrivate();

      //perform RSA private key enctyption of digest with private key
      BigInteger digestNum = new BigInteger(digestBytes);
      BigInteger signature = digestNum.modPow(
         prvk.getPrivateExponent(), prvk.getModulus());
      byte[] signaturebytes = signature.toByteArray();

      System.out.println("Signature(hex, length="+signaturebytes.length+"):" +
        byteArray2Hex(signaturebytes));

      //Now, signaturebytes = signature for the message text.
      //Anybody wanting to verify signature needs original text message
      // + signature bytes + public key

      //verify signature (normally done in different program)
      
      BigInteger sigVerify = new BigInteger(signaturebytes);
      BigInteger chkDigest = sigVerify.modPow(
         pubk.getPublicExponent(), pubk.getModulus());
      byte[] chkDigestBytes = chkDigest.toByteArray();

      //Now, if signature works correctly: digestBytes==chkDigestBytes
       boolean matched = Arrays.equals(digestBytes,chkDigestBytes);
       System.out.println( matched? "Signatures match!" : "Signatures doesn't match");
     }
     catch(Exception ex) {
       ex.printStackTrace();
     }
  }
  private static String byteArray2Hex(byte[] byteArray){
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < byteArray.length; i++){
            String hex = Integer.toHexString(255 & (int)byteArray[i]);
            sb.append( ((i%8)==0 ? "\n " : " ") 
                        + (hex.length()<2 ? ("0"+hex) : hex));
        }
        return sb.toString();
  }
}


Practically, it is necessary to keep a keyset in a keystore. This allows using the same key set repeatedly. Accessing the keystore is integrated with J2EE. Now follows a client example, generating the signature necessary to transmit together with the (unchanged) message and public key. Stored in a file with extension .sig .

Example: simple signature creation using keystore and J2EE API
SignatureKeystoreClient.java

package dk.cryptography;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;

public class SignatureKeystoreClient {
	public static void main(String[] args) { //args[0]=filename..
       if(args.length<1) {
           System.out.println("Argument syntax: <message filename> ");
           return;
       }
       try { //..use keytool to generate key in keystore.. with properties..
             //fx. keytool -genkey -alias keyAlias -keyalg RSA -keypass theKeyPassword
           String keyStorePwd = "keyPass"; //password to access keystore
           String keyAlias = "keyAlias"; //alias indentifying key in keystore
           String testKeyPassword = "theKeyPassword"; //password to acces key
           
           //load keystore from keystore file
           KeyStore keystore = KeyStore.getInstance("JKS"); //creates keystore
           String keyStoreFileName = System.getProperty("user.home")+File.separator+".keystore";
           keystore.load(new FileInputStream(keyStoreFileName), keyStorePwd.toCharArray());
           
           Key testKey = keystore.getKey(keyAlias,testKeyPassword.toCharArray());
           RSAPrivateKey prvk = (RSAPrivateKey)testKey; //assume it's an RSA key
           
           java.security.cert.Certificate cert = keystore.getCertificate(keyAlias);
           Signature signer = Signature.getInstance("SHA1withRSA");
           signer.initSign(prvk);
           
           File f;
           FileInputStream fis = new FileInputStream(f=new File(args[0]));
           byte[] dataBytes = new byte[(int)f.length()]; //small file - read all
           int nread = fis.read(dataBytes); //hopefully nread==fis.length()
           fis.close();
           signer.update(dataBytes);
           
           byte[] signatureBytes = signer.sign();
           
           FileOutputStream out = new FileOutputStream(args[0] + ".sig"); //signature to file..
           out.write(signatureBytes);
           out.close();
       }
       catch(Exception ex) {
           ex.printStackTrace();
       }
    }
}

What happens next is transmission of: The receiver must have all files to verify the signature if the transmitter.



Example: simple signature verification using keystore and J2EE API
SignatureKeystoreVerify.java

package dk.cryptography;

import java.io.File;
import java.io.FileInputStream;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.interfaces.RSAPublicKey;

public class SignatureKeystoreVerify {
    public static void main(String[] args) { //args[0]=filename..
       if(args.length!=3) {
           System.out.println("Argument syntax: <message filename> <.sig file> <cert file>");
           return;
       }
       try { //..use keytool to generate key in keystore.. with properties..
             //fx. keytool -genkey -alias keyAlias -keyalg RSA -keypass theKeyPassword
             //    keytool -export -alias keyAlias -file keyAlias.cert
             //    java SignatureKeystoreVerify message message.sig keyAlias.cert
             
           //load certificate using a X.509 certificate factory           
           FileInputStream certFile = new FileInputStream(args[2]);
           CertificateFactory certX509Fact = CertificateFactory.getInstance("X.509");              
           Certificate cert = certX509Fact.generateCertificate(certFile);
           certFile.close();
           
           RSAPublicKey pubk = (RSAPublicKey)cert.getPublicKey(); //assuming RSA key, get pub key
           Signature signVerifier = Signature.getInstance("SHA1withRSA");
           signVerifier.initVerify(pubk);
           
           File f=new File(args[0]); //read original message data 
           FileInputStream messageStream = new FileInputStream(f);
           byte[] messageBytes = new byte[(int)f.length()]; //small file - read all
           int nread = messageStream.read(messageBytes); //hopefully nread==fis.length()
           messageStream.close();
           signVerifier.update(messageBytes);
           
           f = new File(args[1]); //read client generated signature 
           byte[] signatureBytes = new byte[(int)f.length()];
           FileInputStream signatureStream = new FileInputStream(f);
           nread = signatureStream.read(signatureBytes); //hopefully nread==fis.length()
           signatureStream.close();
           
           System.out.println( signVerifier.verify(signatureBytes) ? "Signatures match!" : "Signatures doesn't match");
       }
       catch(Exception ex) {
           ex.printStackTrace();
       }
    }
}

If the signature matches correctly, a the result reads "Signature match!".

Conclusion

Use of digital signature ensures: J2EE framework allows extensive manipulating of digital signature and key exchange.

/www.cryptography.dk (2004-7-30)

Ref:
http://java.sun.com/developer/onlineTraining/Security/Fundamentals/contents.html