在 JAVA 中使用可信时间戳
可信时间戳是让可信第三方(“时间戳机构”,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 类斗争数天以完成这个所谓的简单任务,这通常很有用。