在 JAVA 中使用可信时间戳

2021-09-24 10:29:06 浏览数 (2743)

可信时间戳是让可信第三方(“时间戳机构”,TSA)以电子形式证明给定事件的时间的过程。欧盟法规 eIDAS 赋予这些时间戳法律效力——即,如果事件带有时间戳,则没有人可以对时间或事件内容提出异议。适用于多种场景,包括时间戳审计日志。(注意:时间戳对于良好的审计跟踪是不够的,因为它不能阻止恶意行为者完全删除事件)

可信时间戳有许多标准,核心之一是​RFC 3161​。与大多数 RFC 一样,它很难阅读。对于 Java 用户来说幸运的是,BouncyCastle 实现了该标准。不幸的是,与大多数安全 API 一样,使用它很困难,甚至很糟糕。我必须实现它,所以我将分享时间戳数据所需的代码。

我将尝试解释主要流程。显然,有很多代码可以简单地遵循标准。BouncyCastle 类是一个难以导航的迷宫。

主要方法显然是​timestamp(hash, tsaURL, username, password, tsaPolicyOid)​:

public TimestampResponseDto timestamp(byte[] hash, String tsaUrl, String tsaUsername, String tsaPassword, String tsaPolicyOid)throws IOException {
    MessageImprint imprint =new MessageImprint(sha512oid, hash);
 
    ASN1ObjectIdentifier tsaPolicyId = StringUtils.isNotBlank(tsaPolicyOid) ?new ASN1ObjectIdentifier(tsaPolicyOid) : baseTsaPolicyId;
 
    TimeStampReq request =new TimeStampReq(imprint, tsaPolicyOid,new ASN1Integer(random.nextLong()),
            ASN1Boolean.TRUE,null);
 
    byte[] body = request.getEncoded();
    try {
        byte[] responseBytes = getTSAResponse(body, tsaUrl, tsaUsername, tsaPassword);
 
        ASN1StreamParser asn1Sp =new ASN1StreamParser(responseBytes);
        TimeStampResp tspResp = TimeStampResp.getInstance(asn1Sp.readObject());
        TimeStampResponse tsr =new TimeStampResponse(tspResp);
 
        checkForErrors(tsaUrl, tsr);
 
        // validate communication level attributes (RFC 3161 PKIStatus)
        tsr.validate(new TimeStampRequest(request));
 
        TimeStampToken token = tsr.getTimeStampToken();
             
        TimestampResponseDto response =new TimestampResponseDto();
        response.setTime(getSigningTime(token.getSignedAttributes()));
        response.setEncodedToken(Base64.getEncoder().encodeToString(token.getEncoded()));
            
        return response;
    }catch (RestClientException | TSPException | CMSException | OperatorCreationException | GeneralSecurityException e) {
        throw new IOException(e);
    }
}

它通过创建消息印记来准备请求。请注意,您传递的是散列本身,还有用于生成散列的散列算法。为什么 API 不对你隐藏它,我不知道。在我的情况下,哈希以更复杂的方式获得,因此它很有用,而且仍然很实用。然后我们获取请求的原始形式并将其发送给 TSA(时间戳机构)。这是一个 HTTP 请求,有点简单,但您必须处理一些在 TSA 中不一定一致的请求和响应标头。用户名和密码是可选的,一些 TSA 提供无需身份验证的服务(限速)。另请注意 ​tsaPolicyOid ​– 大多数 TSA 都有其特定政策记录在其页面上,您应该从那里获取 OID。

当您返回原始响应时,您将其解析为 ​TimeStampResponse​。同样,您必须经过 2 个中间对象(​ASN1StreamParser​ 和 ​TimeStampResp​),这可能是一个适当的抽象,但不是可用的 API。

然后您检查响应是否成功,您还必须对其进行验证——TSA 可能返回了错误的响应。理想情况下,所有这些都可以对您隐藏。验证会引发异常,在这种情况下,我只是通过包装在 IOException 中进行传播。

最后,您获得令牌并返回响应。最重要的是令牌的内容,在我的情况下需要 Base64,所以我对其进行了编码。它也可能只是原始字节。如果你想从令牌中获取任何额外的数据(例如签名时间),并不是那么简单;您必须解析低级属性(见要点)。

好的,您现在拥有令牌,可以将其存储在数据库中。有时您可能想要验证时间戳是否未被篡改。代码在这里,我甚至不会试图解释它——这是一大堆样板,也说明了 TSA 响应方式的变化(我已经尝试了一些)。需要 DummyCertificate 类的事实要么意味着我做错了什么,要么证实了我对 BouncyCastle API 的批评。某些 TSA 可能不需要 DummyCertificate,但其他 TSA 需要 DummyCertificate,而且您实际上无法那么容易地实例化它。您需要一个真实的证书来构建它(它不包含在要点中;使用下一个要点中的 init() 方法,您可以使用​dummyCertificate = new DummyCertificate(certificateHolder.toASN1Structure());​)。在我的代码中,这些都是一个类,但是为了呈现它们,我决定将其拆分,因此有一点重复。

好的,现在我们可以添加时间戳并验证时间戳。这应该足够了;但出于测试目的(或有限的内部使用),您可能希望在本地进行时间戳记,而不是询问 TSA。代码可以在这里找到。它使用 spring,但您可以将密钥库详细信息作为参数传递给 init 方法。您需要一个带有密钥对和证书的 JKS 存储,我使用 ​KeyStore Explorer​ 创建它们。如果您在 AWS 中运行您的应用程序,您可能希望使用 KMS(密钥管理服务)加密您的密钥库,然后在应用程序加载时对其进行解密,但这超出了本文的范围。对于本地时间戳验证按预期工作,对于时间戳 – 无需调用外部服务,只需调用localTSA.timestamp(req);

我是如何知道要实例化哪些类以及要传递哪些参数的——我不记得了。查看测试、示例、答案、来源。花了一段时间,所以我分享它,以潜在地避免其他人的一些麻烦。

您可以测试的TSA列表:​SafeCreative​、​FreeTSA​、​time.centum.pl​。

我意识到这似乎不适用于许多场景,但我建议为应用程序数据的一些关键部分添加时间戳。将它放在你的“工具箱”中,随时可用,而不是试图阅读标准并与 BouncyCastle 类斗争数天以完成这个所谓的简单任务,这通常很有用。