Digital Signatures in Java
In this tutorial, we’re going to learn about the Digital Signature mechanism and how we can implement it using the Java Cryptography Architecture (JCA). We’ll explore the KeyPair, MessageDigest, Cipher, KeyStore, Certificate, and Signature JCA APIs.
We’ll start by understanding what is Digital Signature, how to generate a key pair, and how to certify the public key from a certificate authority (CA). After that, we’ll see how to implement Digital Signature using the low-level and high-level JCA APIs.
2. What Is Digital Signature?
Integrity: the message hasn’t been altered in transit
Authenticity: the author of the message is really who they claim to be
Non-repudiation: the author of the message can’t later deny that they were the source
2.2. Sending a Message with a Digital Signature
Technically speaking, a digital signature is the encrypted hash (digest, checksum) of a message. That means we generate a hash from a message and encrypt it with a private key according to a chosen algorithm.
The message, the encrypted hash, the corresponding public key, and the algorithm are all then sent. This is classified as a message with its digital signature.
2.3. Receiving and Checking a Digital Signature
To check the digital signature, the message receiver generates a new hash from the received message, decrypts the received encrypted hash using the public key, and compares them. If they match, the Digital Signature is said to be verified.
We should note that we only encrypt the message hash, and not the message itself. In other words, Digital Signature doesn’t try to keep the message secret. Our digital signature only proves that the message was not altered in transit.
When the signature is verified, we’re sure that only the owner of the private key could be the author of the message.
3. Digital Certificate and Public Key Identity
We know that if the hash we decrypt with the published public key matches the actual hash, then the message is signed. However, how do we know that the public key really came from the right entity? This is solved by the use of digital certificates.
A Digital Certificate contains a public key and is itself signed by another entity. The signature of that entity can itself be verified by another entity and so on. We end up having what we call a certificate chain. Each top entity certifies the public key of the next entity. The most top-level entity is self-signed, which means that his public key is signed by his own private key.
The X.509 is the most used certificate format, and it is shipped either as binary format (DER) or text format (PEM). JCA already provides an implementation for this via the X509Certificate class.
4. KeyPair Management
4.1. Getting a KeyPair
To create a key pair of a private and public key, we’ll use the Java keytool.
Let’s generate a key pair using the genkeypair command:
keytool -genkeypair -alias senderKeyPair -keyalg RSA -keysize 2048 \ -dname "CN=Baeldung" -validity 365 -storetype PKCS12 \ -keystore sender_keystore.p12 -storepass changeit
This creates a private key and its corresponding public key for us. The public key is wrapped into an X.509 self-signed certificate which is wrapped in turn into a single-element certificate chain. We store the certificate chain and the private key in the Keystore file sender_keystore.p12, which we can process using the KeyStore API.
Here, we’ve used the PKCS12 key store format, as it is the standard and recommended over the Java-proprietary JKS format. Also, we should remember the password and alias, as we’ll use them in the next subsection when loading the Keystore file.
4.2. Loading the Private Key for Signing
Using the KeyStore API, and the previous Keystore file, sender_keystore.p12, we can get a PrivateKey object:
KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("sender_keystore.p12"), "changeit"); PrivateKey privateKey = (PrivateKey) keyStore.getKey("senderKeyPair", "changeit");
4.3. Publishing the Public Key
When using a self-signed certificate, we need only to export it from the Keystore file. We can do this with the exportcert command:
keytool -exportcert -alias senderKeyPair -storetype PKCS12 \ -keystore sender_keystore.p12 -file \ sender_certificate.cer -rfc -storepass changeit
Otherwise, if we’re going to work with a CA-signed certificate, then we need to create a certificate signing request (CSR). We do this with the certreq command:
keytool -certreq -alias senderKeyPair -storetype PKCS12 \ -keystore sender_keystore.p12 -file -rfc \ -storepass changeit > sender_certificate.csr
The CSR file, sender_certificate.csr, is then sent to a Certificate Authority for the purpose of signing. When this is done, we’ll receive a signed public key wrapped in an X.509 certificate, either in binary (DER) or text (PEM) format. Here, we’ve used the rfc option for a PEM format.
The public key we received from the CA, sender_certificate.cer, has now been signed by a CA and can be made available for clients.
4.4. Loading a Public Key for Verification
keytool -importcert -alias receiverKeyPair -storetype PKCS12 \ -keystore receiver_keystore.p12 -file \ sender_certificate.cer -rfc -storepass changeit
And using the KeyStore API as before, we can get a PublicKey instance:
KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("receiver_keytore.p12"), "changeit"); Certificate certificate = keyStore.getCertificate("receiverKeyPair"); PublicKey publicKey = certificate.getPublicKey();
Now that we have a PrivateKey instance on the sender side, and an instance of the PublicKey on the receiver side, we can start the process of signing and verification.
5. Digital Signature with MessageDigest and Cipher Classes
Now, let’s start implementing the digital signature mechanisms.
5.1. Generating a Message Hash
byte messageBytes = Files.readAllBytes(Paths.get("message.txt"));
Now, using MessageDigest, let’s use the digest method to generate a hash:
MessageDigest md = MessageDigest.getInstance("SHA-256"); byte messageHash = md.digest(messageBytes);
Here, we’ve used the SHA-256 algorithm, which is the one most commonly used. Other alternatives are MD5, SHA-384, and SHA-512.
5.2. Encrypting the Generated Hash
Let’s create a Cipher instance and initialize it for encryption. Then we’ll call the doFinal() method to encrypt the previously hashed message:
Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, privateKey); byte digitalSignature = cipher.doFinal(messageHash);
The signature can be saved into a file for sending it later:
At this point, the message, the digital signature, the public key, and the algorithm are all sent, and the receiver can use these pieces of information to verify the integrity of the message.
5.3. Verifying Signature
Let’s read the received digital signature:
byte encryptedMessageHash = Files.readAllBytes(Paths.get("digital_signature_1"));
For decryption, we create a Cipher instance. Then we call the doFinal method:
Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, publicKey); byte decryptedMessageHash = cipher.doFinal(encryptedMessageHash);
Next, we generate a new message hash from the received message:
byte messageBytes = Files.readAllBytes(Paths.get("message.txt")); MessageDigest md = MessageDigest.getInstance("SHA-256"); byte newMessageHash = md.digest(messageBytes);
And finally, we check if the newly generated message hash matches the decrypted one:
boolean isCorrect = Arrays.equals(decryptedMessageHash, newMessageHash);
In this example, we’ve used the text file message.txt to simulate a message we want to send, or the location of the body of a message we’ve received. Normally, we’d expect to receive our message alongside the signature.
6. Digital Signature Using the Signature Class
However, JCA already offers a dedicated API in the form of the Signature class.
6.1. Signing a Message
Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey);
The signing algorithm we chose, SHA256withRSA in this example, is a combination of a hashing algorithm and an encryption algorithm. Other alternatives include SHA1withRSA, SHA1withDSA, and MD5withRSA, among others.
Next, we proceed to sign the byte array of the message:
byte messageBytes = Files.readAllBytes(Paths.get("message.txt")); signature.update(messageBytes); byte digitalSignature = signature.sign();
We can save the signature into a file for later transmission:
6.2. Verifying the Signature
Signature signature = Signature.getInstance("SHA256withRSA");
Next, we initialize the Signature object for verification by calling the initVerify method, which takes a public key:
Then, we need to add the received message bytes to the signature object by invoking the update method:
byte messageBytes = Files.readAllBytes(Paths.get("message.txt")); signature.update(messageBytes);
And finally, we can call check the signature by calling the verify method:
boolean isCorrect = signature.verify(receivedSignature);
In this article, we first looked at how digital signature works and how to establish trust for a digital certificate. Then we implemented a digital signature using the MessageDigest, Cipher, and Signature classes from the Java Cryptography Architecture.
We saw in detail how to sign data using the private key and how to verify the signature using a public key.
As always, the code from this article is available over on GitHub.