3
votes

I'm able to digitally sign a PDF document using PDFBOX 1.8.5 thanks to this excellent sample, provided within PDFBOX.

https://github.com/apache/pdfbox/blob/1.8/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignature.java

When signing this sample use the date/time of the local machine (Line 175):

// the signing date, needed for valid signature signature.setSignDate(Calendar.getInstance());

It means Acrobat Reader will not trust the signature date as if it were done using a external Time Stamp Authority (TSA).

Anyone know how to use a external TSA with PDFBOX ?

Thanks.

1
1) please update to the current version (1.8.8), there were some bugfixes to "structural" parts of PDFs. 2) please tell whether the answer of mkl is good (I think it is), or whether there are any problems / any further help needed.Tilman Hausherr

1 Answers

3
votes

The CreateSignature PDFBox example has been extended in the 2.0.0-SNAPSHOT development version to also optionally include a time stamp from some TSA.

The main difference is that after creating the CMS signature in sign(InputStream), the CMS signature container is enhanced in an additional method signTimeStamps(CMSSignedData) to also carry a signature time stamp:

public byte[] sign(InputStream content) throws IOException
{
    ...
        CMSSignedData signedData = gen.generate(processable, false);
        // vvv Additional call
        if (tsaClient != null)
        {
            signedData = signTimeStamps(signedData);
        }
        // ^^^ Additional call
        return signedData.getEncoded();
    ...
}

// vvv Additional helper methods
private CMSSignedData signTimeStamps(CMSSignedData signedData)
        throws IOException, TSPException
{
    SignerInformationStore signerStore = signedData.getSignerInfos();
    List<SignerInformation> newSigners = new ArrayList<SignerInformation>();

    for (SignerInformation signer : (Collection<SignerInformation>)signerStore.getSigners())
    {
        newSigners.add(signTimeStamp(signer));
    }

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

private SignerInformation signTimeStamp(SignerInformation signer)
        throws IOException, TSPException
{
    AttributeTable unsignedAttributes = signer.getUnsignedAttributes();

    ASN1EncodableVector vector = new ASN1EncodableVector();
    if (unsignedAttributes != null)
    {
        vector = unsignedAttributes.toASN1EncodableVector();
    }

    byte[] token = tsaClient.getTimeStampToken(signer.getSignature());
    ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
    ASN1Encodable signatureTimeStamp = new Attribute(oid, new DERSet(byteToASN1Object(token)));

    vector.add(signatureTimeStamp);
    Attributes signedAttributes = new Attributes(vector);

    SignerInformation newSigner = SignerInformation.replaceUnsignedAttributes(
            signer, new AttributeTable(signedAttributes));

    if (newSigner == null)
    {
        return signer;
    }

    return newSigner;
}

private ASN1Object byteToASN1Object(byte[] data) throws IOException
{
    ASN1InputStream in = new ASN1InputStream(data);
    try
    {
        return in.readObject();
    }
    finally
    {
        in.close();
    }
}
// ^^^ Additional helper methods

(CreateSignature.java, 2.0.0-SNAPSHOT development version)

Here tsaClient is a new CreateSignature member variable containing a TSAClient instance interfacing the external TSA in question:

/**
 * Time Stamping Authority (TSA) Client [RFC 3161].
 * @author Vakhtang Koroghlishvili
 * @author John Hewson
 */
public class TSAClient
{
    private static final Log log = LogFactory.getLog(TSAClient.class);

    private final URL url;
    private final String username;
    private final String password;
    private final MessageDigest digest;

    public TSAClient(URL url, String username, String password, MessageDigest digest)
    {
        this.url = url;
        this.username = username;
        this.password = password;
        this.digest = digest;
    }

    public byte[] getTimeStampToken(byte[] messageImprint) throws IOException
    {
        digest.reset();
        byte[] hash = digest.digest(messageImprint);

        // 32-bit cryptographic nonce
        SecureRandom random = new SecureRandom();
        int nonce = random.nextInt();

        // generate TSA request
        TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
        tsaGenerator.setCertReq(true);
        ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
        TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));

        // get TSA response
        byte[] tsaResponse = getTSAResponse(request.getEncoded());

        TimeStampResponse response;
        try
        {
            response = new TimeStampResponse(tsaResponse);
            response.validate(request);
        }
        catch (TSPException e)
        {
            throw new IOException(e);
        }

        TimeStampToken token = response.getTimeStampToken();
        if (token == null)
        {
            throw new IOException("Response does not have a time stamp token");
        }

        return token.getEncoded();
    }

    // gets response data for the given encoded TimeStampRequest data
    // throws IOException if a connection to the TSA cannot be established
    private byte[] getTSAResponse(byte[] request) throws IOException
    {
        log.debug("Opening connection to TSA server");

        // todo: support proxy servers
        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 (username != null && password != null)
        {
            if (!username.isEmpty() && !password.isEmpty())
            {
                connection.setRequestProperty(username, 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;
    }

    // returns the ASN.1 OID of the given hash algorithm
    private ASN1ObjectIdentifier getHashObjectIdentifier(String algorithm)
    {
        // TODO can bouncy castle or Java provide this information?
        if (algorithm.equals("MD2"))
        {
            return new ASN1ObjectIdentifier("1.2.840.113549.2.2");
        }
        else if (algorithm.equals("MD5"))
        {
            return new ASN1ObjectIdentifier("1.2.840.113549.2.5");
        }
        else if (algorithm.equals("SHA-1"))
        {
            return new ASN1ObjectIdentifier("1.3.14.3.2.26");
        }
        else if (algorithm.equals("SHA-224"))
        {
            return new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.4");
        }
        else if (algorithm.equals("SHA-256"))
        {
            return new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.1");
        }
        else if (algorithm.equals("SHA-394"))
        {
            return new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.2");
        }
        else if (algorithm.equals("SHA-512"))
        {
            return new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.3");
        }
        else
        {
            return new ASN1ObjectIdentifier(algorithm);
        }
    }
}

(TSAClient.java, 2.0.0-SNAPSHOT development version)

As these additions only depend on the BouncyCastle version used but not PDFBox code, this code should also be easy to backport for use with PDFBox 1.8.x.