【问题标题】:PDFBox 1.8 - Signing PDF Document using TSA (Time Stamp Autority)PDFBox 1.8 - 使用 TSA(时间戳授权)签署 PDF 文档
【发布时间】:2015-02-11 00:15:22
【问题描述】:

感谢 PDFBOX 中提供的这个出色的示例,我能够使用 PDFBOX 1.8.5 对 PDF 文档进行数字签名。

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

签署此示例时,使用本地机器的日期/时间(第 175 行):

// 签名日期,有效签名所需 签名.setSignDate(Calendar.getInstance());

这意味着 Acrobat Reader 不会信任签名日期,就好像它是使用外部时间戳授权 (TSA) 完成的一样。

有人知道如何在 PDFBOX 中使用外部 TSA 吗?

谢谢。

【问题讨论】:

  • 1) 请更新到当前版本 (1.8.8),对 PDF 的“结构”部分进行了一些错误修复。 2)请告诉mkl的答案是否好(我认为是),或者是否有任何问题/需要任何进一步的帮助。

标签: timestamp pdfbox sign


【解决方案1】:

CreateSignature PDFBox 示例已在 2.0.0-SNAPSHOT 开发版本中进行了扩展,还可以选择包含来自某些 TSA 的时间戳。

主要区别在于在sign(InputStream)中创建CMS签名后,CMS签名容器在signTimeStamps(CMSSignedData)的附加方法中得到了增强,也带有签名时间戳:

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 开发版)

这里的tsaClient 是一个新的CreateSignature 成员变量,其中包含一个与相关外部TSA 接口的TSAClient 实例:

/**
 * 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 开发版)

由于这些添加仅取决于所使用的 BouncyCastle 版本而不是 PDFBox 代码,因此该代码也应该很容易向后移植以与 PDFBox 1.8.x 一起使用。

【讨论】:

  • 你显然想用ASN1Object.fromByteArray(byte[])而不是你的byteToASN1Object(byte[])
  • 好吧,PDFBox 开发人员可能会感兴趣。
  • 这取决于 BouncyCastle 的版本。不幸的是,他们改变了很多 API,不幸的是 PdfBox 使用 BC 1.44 版本。
  • @divanov 我已经提交了您对使用 1.51 的主干的更改。但是你对 1.8 是正确的。我正在等待 Romain 的一些反馈,然后我应该考虑根据 mkl 提出的论点将代码放入 1.8。
  • 有人可以分享一个传递给 CreateSignature 类的 main 方法的参数示例
猜你喜欢
  • 2015-05-27
  • 2021-11-24
  • 1970-01-01
  • 2015-08-04
  • 2014-02-06
  • 2019-12-17
  • 1970-01-01
  • 2018-12-02
  • 1970-01-01
相关资源
最近更新 更多