In a recent post, I showed you how to digitally sign PDF files and as I mentioned, today we will add Timestamp to our signature.
What do we want to achieve?
We want to export fully signed PDF files, with a valid timestamp, as follows:
Source code
The full example is available on my Github.
Why we need Timestamp?
As we already know, every certificate has an expiration date, so when the expiration date passed, the certificate should not be valid any longer – it’s clear.
Let’s imagine that someone, e.g. Josh, has a certificate valid until 31.12.2018. Josh can digitally sign PDFs and send these files to his clients, but in 2019 Josh got a message, that signature isn’t valid. Josh is quite clever, so he changed the date on his computer to 01.01.2018. From now, every signature is valid. Quite simple, huh? Of course, it’s the simplest example. You can find more theoretical knowledge in an awesome document created by iText (check the recent post for more info).
Timestamp Authority
The situation with Timestamp is quite similar to the situation with certificates. If you want to ensure full security, you should obtain a proper Timestamp from Timestamp Authority.
You can ask, but how it works? I need a fetch current time, so it is still obtained from my local computer?
The answer is probably simpler than you can imagine. If a certificate contains Timestamp, then in most cases means, that there you can find a proper URL. So everything that you need is to make a request to the URL and retrieve TimeStampToken.
Adding Timestamp to the project
We have already configured and working project, so we just need to add several lines. Let’s create a TSAClient.java.
Our client should contain several pieces of information, for sure URL where we will request for Time Stamp. As you can imagine, some URL can required authentication, so for this purpose, we will keep username and password. The last field is a message digest. What it is? It’s just a cryptographic hash function containing a string of digits created by a one-way hashing mechanism.
The MessageDigest class comes from java.security package, and above this class, you can find information as follows:
/**
* This MessageDigest class provides applications the functionality of a
* message digest algorithm, such as SHA-1 or SHA-256.
* Message digests are secure one-way hash functions that take arbitrary-sized
* data and output a fixed-length hash value.
* (…)
We will create an object of this class and use this to hashing our signature before will be sent to the Timestamp Authority.
For now, the TSAClient.java looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class TSAClient { private final URL url; private final String username; private final String password; private final MessageDigest digest; TSAClient(URL url, String username, String password, MessageDigest digest) { this.url = url; this.username = username; this.password = password; this.digest = digest; } } |
Feel free to use Lombok annotation @AllArgsConstructor, I left plain constructor for putting some comments above (see my Github).
Ok, we have all needed information about TSA, so now we can implement getTimeStampToken method. You can treat this method as a template, we just have to do some steps to get the response.
First of all, we need to generate a hash from our signature, then we have to generate a nonce, it’s random number issued in an authentication protocol to ensure that old communications cannot be reused in replay attacks, in our example it will be a random number generated by SecureRandom object from java.security package. After that, we can create TimeStampRequestGenerator, next we will create an object identifier and finally TimeStampRequest. We need to establish the connection to the proper URL, if necessary, then authenticate with our username and password, and finally fetch data (token).
Sounds easy, and it is. Look at the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
byte[] getTimeStampToken(byte[] messageImprint) throws IOException, TSPException { this.digest.reset(); byte[] hash = this.digest.digest(messageImprint); // generate cryptographic nonce SecureRandom random = new SecureRandom(); int nonce = random.nextInt(); // generate TSA request TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator(); tsaGenerator.setCertReq(true); ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId()); TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce)); // get TSA response byte[] tsaResponse = getTSAResponse(request.getEncoded()); TimeStampResponse response = new TimeStampResponse(tsaResponse); response.validate(request); TimeStampToken token = response.getTimeStampToken(); if (token == null) { throw new IOException("Response does not have a time stamp token"); } return token.getEncoded(); } |
The getTSAResponse method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
private byte[] getTSAResponse(byte[] request) throws IOException { log.debug("Opening connection to TSA server"); URLConnection connection = url.openConnection(); connection.setDoOutput(true); connection.setDoInput(true); connection.setRequestProperty("Content-Type", "application/timestamp-query"); log.debug("Established connection to TSA server"); if (Strings.isNotBlank(this.username) && Strings.isNotBlank(this.password)) { connection.setRequestProperty(this.username, this.password); } // read response OutputStream output = null; try { output = connection.getOutputStream(); output.write(request); } finally { IOUtils.closeQuietly(output); } log.debug("Waiting for response from TSA server"); InputStream input = null; byte[] response; try { input = connection.getInputStream(); response = IOUtils.toByteArray(input); } finally { IOUtils.closeQuietly(input); } log.debug("Received response from TSA server"); return response; } |
The very important line here is:
connection.setRequestProperty(“Content-Type”, “application/timestamp-query”);
The content type has to be application/timestamp-query, if you don’t set it up, you will not obtain data from TSA!
It’s time for TimeStampManager. In this class, we want to implement a method which allows adding a timestamp to our signature.
We need to add a signature for every signer, as you already know the signature can contain several signers, so we will fetch every signer from CMSSignedData.
When every signer will contain the timestamp, then we will replace the old ones with the new ones.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CMSSignedData addSignedTimeStamp(CMSSignedData signedData) throws IOException, TSPException { SignerInformationStore signerStore = signedData.getSignerInfos(); List<SignerInformation> signersWithTimeStamp = new ArrayList<>(); for (SignerInformation signer : signerStore.getSigners()) { // This adds a timestamp to every signer (into his unsigned attributes) in the signature. signersWithTimeStamp.add(signTimeStamp(signer)); } // new SignerInformationStore have to be created cause new SignerInformation instance // also SignerInformationStore have to be replaced in a signedData return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(signersWithTimeStamp)); } |
In the signTimeStamp method, we want to replace unsigned signer attributes by the signed, in this function, we will use the already implemented method getTimeStampToken.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private SignerInformation signTimeStamp(SignerInformation signer) throws IOException, TSPException { AttributeTable unsignedAttributes = signer.getUnsignedAttributes(); ASN1EncodableVector vector = new ASN1EncodableVector(); if (unsignedAttributes != null) { vector = unsignedAttributes.toASN1EncodableVector(); } byte[] token = this.tsaClient.getTimeStampToken(signer.getSignature()); ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken; ASN1Encodable signatureTimeStamp = new Attribute(oid, new DERSet(ASN1Primitive.fromByteArray(token))); vector.add(signatureTimeStamp); Attributes signedAttributes = new Attributes(vector); // replace unsignedAttributes with the signed once return SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(signedAttributes)); } |
That’s it! We can use our TimeStampManager in a sign method.
1 2 3 4 |
if (Strings.isNotBlank(this.tsaUrl)) { TimeStampManager timeStampManager = new TimeStampManager(this.tsaUrl); signedData = timeStampManager.addSignedTimeStamp(signedData); } |
You can find the full class here.
The last step is finding a free Time Stamp Authority example, I chose http://sha256timestamp.ws.symantec.com/sha256/timestamp
I’m keeping this URL in an application.yml, but feel free to change the location, remember to put this URL to SigningService.
Result
When you enter a http://localhost:8080/api/pdf/export in your browser you obtain a signed PDF with the timestamp. Let’s open the document.
Almost ok… as always when the certificate or timestamp isn’t trusted by Adobe, we have to handle that.
We have two ways to fix that. I will show you the workaround, but you can just add a proper certificate from Symantec to your trusted certificates in the same way as in the recent post.
- Open any PDF, even not signed
- Go to the Tools tab
- Search for Certificates
- Click Time Stamp -> New
- Set any name, and Timestamp Authority URL (e.g. http://sha256timestamp.ws.symantec.com/sha256/timestamp)
- Set this Timestamp Server as Default
- Allow for connection with the server if popup occurs
- The last step, add Timestamp Authority from the Adobe Reader level
- Export file once again
Finally, we got it!
Summary
We’ve just added the timestamp to our signature, it’s a big improvement and we achieved full-featured signature.
Everything that you have to do to use this code on the production is a certificate replacement to the one of authorized by Adobe, then you can send these files to anyone and the signature will be valid.
I hope that you liked it. Let me know if you have any question!
The next post coming soon, stay tuned!
Leave a Reply