0

I'm trying to add a timestamp to my signature on a PDF document using PDFBox 1.8.9. I'm getting errors saying the timestamp cannot be verified, and when I try to check the certificate of the timestamping authority it appears as "Not available".

Here is my method sign in a class that implements SignatureInterface, it calls the method SignTimeStamps

public byte[] sign(java.io.InputStream content)
    {
        CMSProcessableInputStream input = new CMSProcessableInputStream(content);
        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        // Certificate Chain
        java.util.List certList = java.util.Arrays.asList(certificates);

        java.security.cert.CertStore certStore = null;

        try
        {
            certStore = java.security.cert.CertStore.getInstance("Collection", new java.security.cert.CollectionCertStoreParameters(certList), provider);
            gen.addSigner(privateKey, (java.security.cert.X509Certificate)certList.get(0), CMSSignedGenerator.DIGEST_SHA256);
            gen.addCertificatesAndCRLs(certStore);
            CMSSignedData signedData = gen.generate(input, false, provider);
            if (signatureResources.TsaUrl != null)
            {
                // signedData = AddTimeStamp(signedData);
                signedData = SignTimeStamps(signedData);
            }
            return signedData.getEncoded();
        }
        catch (java.lang.Exception e)
        {
            // should be handled
            System.Console.WriteLine("error while creating pkcs7 signature");
            e.printStackTrace();
        }

        throw new java.lang.RuntimeException("problem while preparing signature");
    }

The method SignTimeStamps calls the method SignTimeStamp for each SignerInfo

private CMSSignedData SignTimeStamps(CMSSignedData signedData)
{
        SignerInformationStore signerStore = signedData.getSignerInfos();
        java.util.Iterator iterator = ((java.util.Collection)signerStore.getSigners()).iterator();
        java.util.List newSigners = new java.util.ArrayList();

        while (iterator.hasNext())
        {
            newSigners.add(SignTimeStamp((SignerInformation)iterator.next()));
        }

        return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
}

And then we get the signature from the SignerInfo which is the message imprint for our TimeStampToken, as specified in Appendix A of RFC 3161

private SignerInformation SignTimeStamp(SignerInformation signer)
{
        AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
        ASN1EncodableVector vector = new ASN1EncodableVector();
        if (unsignedAttributes != null)
        {
            vector = unsignedAttributes.toASN1EncodableVector();
        }

        TSAClientBouncyCastle tsaClient = new TSAClientBouncyCastle(signatureResources.TsaUrl);
        byte[] token = tsaClient.GetTimeStampToken(signer.getSignature());
        DERObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
        ASN1Encodable signatureTimeStamp = new org.bouncycastle.asn1.cms.Attribute(oid, new DERSet(ASN1Object.fromByteArray(token)));
        vector.add(signatureTimeStamp);
        SignerInformation newSigner = SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(vector));
        if (newSigner == null) return signer;
        return newSigner;
}

So I'm not really sure what I'm doing wrong, what can I do so that the timestamp can be verified and the timestamping authority appears as available?


EDIT Here is a link to a PDF file I signed https://www.dropbox.com/s/al3h8lorqisi34q/dummy_signed.pdf?dl=1. The TSA is a free TSA (http://time.certum.pl/)


EDIT 2

Here is my TSAClientBouncyCastle.cs file

using System;
using System.IO;
using System.Collections;
using System.Net;
using System.Text;
using System.util;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Tsp;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1.Cmp;
using Org.BouncyCastle.Asn1.Tsp;
using Org.BouncyCastle.Crypto;

namespace ProofOfConcept {
public class TSAClientBouncyCastle : ITSAClient {

    /** The Logger instance. */
    private static readonly ILogger LOGGER = LoggerFactory.GetLogger(typeof(TSAClientBouncyCastle));

    /** URL of the Time Stamp Authority */
    protected internal String tsaURL;
    /** TSA Username */
    protected internal String tsaUsername;
    /** TSA password */
    protected internal String tsaPassword;
    /** An interface that allows you to inspect the timestamp info. */
    protected ITSAInfoBouncyCastle tsaInfo;
    /** The default value for the hash algorithm */
    public const int DEFAULTTOKENSIZE = 4096;
    
    /** Estimate of the received time stamp token */
    protected internal int tokenSizeEstimate;
    
    /** The default value for the hash algorithm */
    public const String DEFAULTHASHALGORITHM = "SHA-256";
    
    /** Hash algorithm */
    protected internal String digestAlgorithm;
    /**
    * Creates an instance of a TSAClient that will use BouncyCastle.
    * @param url String - Time Stamp Authority URL (i.e. "http://tsatest1.digistamp.com/TSA")
    */
    public TSAClientBouncyCastle(String url) 
        : this(url, null, null, DEFAULTTOKENSIZE, DEFAULTHASHALGORITHM) {
    }
    
    /**
    * Creates an instance of a TSAClient that will use BouncyCastle.
    * @param url String - Time Stamp Authority URL (i.e. "http://tsatest1.digistamp.com/TSA")
    * @param username String - user(account) name
    * @param password String - password
    */
    public TSAClientBouncyCastle(String url, String username, String password) 
        : this(url, username, password, DEFAULTTOKENSIZE, DEFAULTHASHALGORITHM) {
    }
    
    /**
    * Constructor.
    * Note the token size estimate is updated by each call, as the token
    * size is not likely to change (as long as we call the same TSA using
    * the same imprint length).
    * @param url String - Time Stamp Authority URL (i.e. "http://tsatest1.digistamp.com/TSA")
    * @param username String - user(account) name
    * @param password String - password
    * @param tokSzEstimate int - estimated size of received time stamp token (DER encoded)
    */
    public TSAClientBouncyCastle(String url, String username, String password, int tokSzEstimate, String digestAlgorithm) {
        this.tsaURL       = url;
        this.tsaUsername  = username;
        this.tsaPassword  = password;
        this.tokenSizeEstimate = tokSzEstimate;
        this.digestAlgorithm = digestAlgorithm;
    }
    
    /**
     * @param tsaInfo the tsaInfo to set
     */
    public void SetTSAInfo(ITSAInfoBouncyCastle tsaInfo) {
        this.tsaInfo = tsaInfo;
    }

    /**
    * Get the token size estimate.
    * Returned value reflects the result of the last succesfull call, padded
    * @return an estimate of the token size
    */
    public virtual int GetTokenSizeEstimate() {
        return tokenSizeEstimate;
    }
    
    /**
     * Gets the MessageDigest to digest the data imprint
     * @return the digest algorithm name
     */
    public IDigest GetMessageDigest() {
        return DigestAlgorithms.GetMessageDigest(digestAlgorithm);
    }

    /**
     * Get RFC 3161 timeStampToken.
     * Method may return null indicating that timestamp should be skipped.
     * @param imprint data imprint to be time-stamped
     * @return encoded, TSA signed data of the timeStampToken
     */
    public virtual byte[] GetTimeStampToken(byte[] imprint) {
        byte[] respBytes = null;
        // Setup the time stamp request
        TimeStampRequestGenerator tsqGenerator = new TimeStampRequestGenerator();
        tsqGenerator.SetCertReq(true);
        // tsqGenerator.setReqPolicy("1.3.6.1.4.1.601.10.3.1");
        BigInteger nonce = BigInteger.ValueOf(DateTime.Now.Ticks + Environment.TickCount);
        TimeStampRequest request = tsqGenerator.Generate(DigestAlgorithms.GetAllowedDigests(digestAlgorithm), imprint, nonce);
        byte[] requestBytes = request.GetEncoded();
        
        // Call the communications layer
        respBytes = GetTSAResponse(requestBytes);           
        
        // Handle the TSA response
        TimeStampResponse response = new TimeStampResponse(respBytes);
        
        // validate communication level attributes (RFC 3161 PKIStatus)
        response.Validate(request);
        PkiFailureInfo failure = response.GetFailInfo();
        int value = (failure == null) ? 0 : failure.IntValue;
        if (value != 0) {
            // @todo: Translate value of 15 error codes defined by PKIFailureInfo to string
            throw new IOException(MessageLocalization.GetComposedMessage("invalid.tsa.1.response.code.2", tsaURL, value));
        }
        // @todo: validate the time stap certificate chain (if we want
        //        assure we do not sign using an invalid timestamp).
        
        // extract just the time stamp token (removes communication status info)
        TimeStampToken tsToken = response.TimeStampToken;
        if (tsToken == null) {
            throw new IOException(MessageLocalization.GetComposedMessage("tsa.1.failed.to.return.time.stamp.token.2", tsaURL, response.GetStatusString()));
        }
        TimeStampTokenInfo tsTokenInfo = tsToken.TimeStampInfo; // to view details
        byte[] encoded = tsToken.GetEncoded();
        
        LOGGER.Info("Timestamp generated: " + tsTokenInfo.GenTime);
        if (tsaInfo != null) {
            tsaInfo.InspectTimeStampTokenInfo(tsTokenInfo);
        }
        // Update our token size estimate for the next call (padded to be safe)
        this.tokenSizeEstimate = encoded.Length + 32;
        return encoded;
    }
    
    /**
    * Get timestamp token - communications layer
    * @return - byte[] - TSA response, raw bytes (RFC 3161 encoded)
    */
    protected internal virtual byte[] GetTSAResponse(byte[] requestBytes) {
        HttpWebRequest con = (HttpWebRequest)WebRequest.Create(tsaURL);
        con.ContentLength = requestBytes.Length;
        con.ContentType = "application/timestamp-query";
        con.Method = "POST";
        if ((tsaUsername != null) && !tsaUsername.Equals("") ) {
            string authInfo = tsaUsername + ":" + tsaPassword;
            authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo), Base64FormattingOptions.None);
            con.Headers["Authorization"] = "Basic " + authInfo;
        }
        Stream outp = con.GetRequestStream();
        outp.Write(requestBytes, 0, requestBytes.Length);
        outp.Close();
        HttpWebResponse response = (HttpWebResponse)con.GetResponse();
        if (response.StatusCode != HttpStatusCode.OK)
            throw new IOException(MessageLocalization.GetComposedMessage("invalid.http.response.1", (int)response.StatusCode));
        Stream inp = response.GetResponseStream();

        MemoryStream baos = new MemoryStream();
        byte[] buffer = new byte[1024];
        int bytesRead = 0;
        while ((bytesRead = inp.Read(buffer, 0, buffer.Length)) > 0) {
            baos.Write(buffer, 0, bytesRead);
        }
        inp.Close();
        response.Close();
        byte[] respBytes = baos.ToArray();            
        
        String encoding = response.ContentEncoding;
        if (encoding != null && Util.EqualsIgnoreCase(encoding, "base64")) {
            respBytes = Convert.FromBase64String(Encoding.ASCII.GetString(respBytes));
        }
        return respBytes;
    }    
}

EDIT FINAL (SOLUTION)

Thanks to all of you for helping me out, as Tilman Hausherr pointed out, I simply wasn't hashing the signature. I replaced

byte[] token = tsaClient.GetTimeStampToken(signer.getSignature());

in my SignTimeStamp method by

java.security.MessageDigest mda = java.security.MessageDigest.getInstance("SHA-256");
byte[] digest = mda.digest(signer.getSignature());
byte[] token = tsaClient.GetTimeStampToken(digest);

And I can now see the timestamping authority. Thank you again so much!

Simon
  • 1
  • 3
  • Did you include the root certificate of the TSA in the list of trusted certificates in Adobe Reader? Or is this some free TSA service? (These are great to test an application, but usually aren't meant for production) – Tilman Hausherr Jul 13 '20 at 10:32
  • 1
    Can you share an example PDF signed and time stamped by your code, illustrating the issue? – mkl Jul 13 '20 at 10:43
  • @TilmanHausherr It is indeed a free TSA (http://time.certum.pl). I tried to include the TSA in the list of trusted certificates but it did not change anything. It seems like the timestamp doesn't even "know" from what TSA it is since the TSA just says "Not available" – Simon Jul 13 '20 at 14:15
  • @mkl I edited my post to add an example PDF signed and time stamped! – Simon Jul 13 '20 at 14:16
  • That's interesting, at first glance the certificate is included, so at least Adobe should show the certificate. I'll try and look into this later when I have more time. – mkl Jul 13 '20 at 14:42
  • According to ShowSignature.java the timestamp validates up to its root. The main signature is self-signed. I tried this TSA with the 2.0 test code and it worked fine. Could you try to update PDFBox to 1.8.16, and update bouncycastle as much as possible? – Tilman Hausherr Jul 13 '20 at 17:11
  • 1
    eSig DSS claims there is an issue with the time stamp hash. I currently don't have the time to dig deeper myself, though. – mkl Jul 14 '20 at 09:59
  • @TilmanHausherr I don't know if I can, I'm using a port of PDFBox for .NET (http://www.squarepdf.net/pdfbox-in-net) and the latest version available is 1.8.9 – Simon Jul 15 '20 at 07:02
  • @mkl Thank you for the time you already spent helping me out! – Simon Jul 15 '20 at 07:04
  • .NET is not supported. I'm surprised that it signed at all, I remember an issue where I could prove that there is a bug in IKVM (which was discontinued years ago). – Tilman Hausherr Jul 15 '20 at 07:22
  • Oh, that's good to know. I've had a few issues with visual signatures but I can sign a document no problem. – Simon Jul 15 '20 at 07:40
  • It turns out that the ShowSignature example doesn't verify the signature hash in the timestamp. Aaargh. – Tilman Hausherr Jul 16 '20 at 04:21
  • What is the code of TSAClientBouncyCastle ? What algorithm is used for the hash? – Tilman Hausherr Jul 16 '20 at 04:22
  • @TilmanHausherr This is the code of TSAClientBouncyCastle (https://gist.github.com/simjnd/ad5d78bca1d0807422e2908634950841), it's in C# though (could that be a problem?) – Simon Jul 16 '20 at 07:16
  • (and the hash algorithm is SHA-256, sorry) – Simon Jul 16 '20 at 08:22
  • You're not doing a hash; and you can't know that the algorithm is SHA-256. GetTimeStampToken(imprint) looks suspiciously different than the same method in the PDFBox 2.0 examples. You should try to follow this as much as possible. Btw you should include the code in the question, a gist will probably expire. – Tilman Hausherr Jul 16 '20 at 10:21
  • 1
    @TilmanHausherr Oh you're right, thank you so much for pointing it out. I used to do it in a previous version of the method but somehow removed it and didn't realize it. I updated my question to add the code and the modification I made to now send the hashed signature to GetTimeStampToken(imprint) and it now works. Thank you again! – Simon Jul 16 '20 at 11:42
  • 1
    Great to hear that! Please put the final code in an answer instead and include the relevant part of my comment and the one of mkl. (Yes you can answer yourself) – Tilman Hausherr Jul 16 '20 at 11:52

0 Answers0