验证 RFC 3161 可信时间戳
Posted
技术标签:
【中文标题】验证 RFC 3161 可信时间戳【英文标题】:Verify RFC 3161 trusted timestamp 【发布时间】:2013-10-31 22:45:33 【问题描述】:在我的构建过程中,我想包含来自符合 RFC-3161 的 TSA 的时间戳。在运行时,代码将验证这个时间戳,最好不需要第三方库的帮助。 (这是一个 .NET 应用程序,因此我可以随时使用标准哈希和非对称加密功能。)
RFC 3161,它依赖于 ASN.1 和 X.690 等等,实现起来并不简单,所以至少现在,我正在使用 Bouncy Castle 生成 TimeStampReq(请求)并解析 TimeStampResp(回复)。我只是不知道如何验证响应。
到目前为止,我已经弄清楚了如何提取签名本身、公共证书、创建时间戳的时间以及我发送的消息印记摘要和随机数(用于构建时验证)。我不知道如何将这些数据放在一起以生成经过哈希和签名的数据。
这是我在做什么以及我正在尝试做什么的粗略想法。这是测试代码,所以我采取了一些捷径。一旦我得到一些有用的东西,我将不得不清理一些东西并以正确的方式去做。
构建时生成时间戳:
// a lot of fully-qualified type names here to make sure it's clear what I'm using
static void WriteTimestampToBuild()
var dataToTimestamp = Encoding.UTF8.GetBytes("The rain in Spain falls mainly on the plain");
var hashToTimestamp = new System.Security.Cryptography.SHA1Cng().ComputeHash(dataToTimestamp);
var nonce = GetRandomNonce();
var tsr = GetTimestamp(hashToTimestamp, nonce, "http://some.rfc3161-compliant.server");
var tst = tsr.TimeStampToken;
var tsi = tst.TimeStampInfo;
ValidateNonceAndHash(tsi, hashToTimestamp, nonce);
var cms = tst.ToCmsSignedData();
var signer =
cms.GetSignerInfos().GetSigners()
.Cast<Org.BouncyCastle.Cms.SignerInformation>().First();
// TODO: handle multiple signers?
var signature = signer.GetSignature();
var cert =
tst.GetCertificates("Collection").GetMatches(signer.SignerID)
.Cast<Org.BouncyCastle.X509.X509Certificate>().First();
// TODO: handle multiple certs (for one or multiple signers)?
ValidateCert(cert);
var timeString = tsi.TstInfo.GenTime.TimeString;
var time = tsi.GenTime; // not sure which is more useful
// TODO: Do I care about tsi.TstInfo.Accuracy or tsi.GenTimeAccuracy?
var serialNumber = tsi.SerialNumber.ToByteArray(); // do I care?
WriteToBuild(cert.GetEncoded(), signature, timeString/*or time*/, serialNumber);
// TODO: Do I need to store any more values?
static Org.BouncyCastle.Math.BigInteger GetRandomNonce()
var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
var bytes = new byte[10]; // TODO: make it a random length within a range
rng.GetBytes(bytes);
return new Org.BouncyCastle.Math.BigInteger(bytes);
static Org.BouncyCastle.Tsp.TimeStampResponse GetTimestamp(byte[] hash, Org.BouncyCastle.Math.BigInteger nonce, string url)
var reqgen = new Org.BouncyCastle.Tsp.TimeStampRequestGenerator();
reqgen.SetCertReq(true);
var tsrequest = reqgen.Generate(Org.BouncyCastle.Tsp.TspAlgorithms.Sha1, hash, nonce);
var data = tsrequest.GetEncoded();
var webreq = WebRequest.CreateHttp(url);
webreq.Method = "POST";
webreq.ContentType = "application/timestamp-query";
webreq.ContentLength = data.Length;
using(var reqStream = webreq.GetRequestStream())
reqStream.Write(data, 0, data.Length);
using(var respStream = webreq.GetResponse().GetResponseStream())
return new Org.BouncyCastle.Tsp.TimeStampResponse(respStream);
static void ValidateNonceAndHash(Org.BouncyCastle.Tsp.TimeStampTokenInfo tsi, byte[] hashToTimestamp, Org.BouncyCastle.Math.BigInteger nonce)
if(tsi.Nonce != nonce)
throw new Exception("Nonce doesn't match. Man-in-the-middle attack?");
var messageImprintDigest = tsi.GetMessageImprintDigest();
var hashMismatch =
messageImprintDigest.Length != hashToTimestamp.Length ||
Enumerable.Range(0, messageImprintDigest.Length).Any(i=>
messageImprintDigest[i] != hashToTimestamp[i]
);
if(hashMismatch)
throw new Exception("Message imprint doesn't match. Man-in-the-middle attack?");
static void ValidateCert(Org.BouncyCastle.X509.X509Certificate cert)
// not shown, but basic X509Chain validation; throw exception on failure
// TODO: Validate certificate subject and policy
static void WriteToBuild(byte[] cert, byte[] signature, string time/*or DateTime time*/, byte[] serialNumber)
// not shown
运行时的时间戳验证(客户端):
// a lot of fully-qualified type names here to make sure it's clear what I'm using
static void VerifyTimestamp()
var timestampedData = Encoding.UTF8.GetBytes("The rain in Spain falls mainly on the plain");
var timestampedHash = new System.Security.Cryptography.SHA1Cng().ComputeHash(timestampedData);
byte[] certContents;
byte[] signature;
string time; // or DateTime time
byte[] serialNumber;
GetDataStoredDuringBuild(out certContents, out signature, out time, out serialNumber);
var cert = new System.Security.Cryptography.X509Certificates.X509Certificate2(certContents);
ValidateCert(cert);
var signedData = MagicallyCombineThisStuff(timestampedHash, time, serialNumber);
// TODO: What other stuff do I need to magically combine?
VerifySignature(signedData, signature, cert);
// not shown: Use time from timestamp to validate cert for other signed data
static void GetDataStoredDuringBuild(out byte[] certContents, out byte[] signature, out string/*or DateTime*/ time, out byte[] serialNumber)
// not shown
static void ValidateCert(System.Security.Cryptography.X509Certificates.X509Certificate2 cert)
// not shown, but basic X509Chain validation; throw exception on failure
static byte[] MagicallyCombineThisStuff(byte[] timestampedhash, string/*or DateTime*/ time, byte[] serialNumber)
// HELP!
static void VerifySignature(byte[] signedData, byte[] signature, System.Security.Cryptography.X509Certificates.X509Certificate2 cert)
var key = (RSACryptoServiceProvider)cert.PublicKey.Key;
// TODO: Handle DSA keys, too
var okay = key.VerifyData(signedData, CryptoConfig.MapNameToOID("SHA1"), signature);
// TODO: Make sure to use the same hash algorithm as the TSA
if(!okay)
throw new Exception("Timestamp doesn't match! Don't trust this!");
正如你可能猜到的,我认为我被卡住的地方是 MagicallyCombineThisStuff
函数。
【问题讨论】:
【参考方案1】:我终于自己弄明白了。这应该不足为奇,但答案非常复杂和间接。
在 RFC 5652 中缺少的部分。直到我阅读(嗯,略读)那个文档之前,我才真正理解 TimeStampResp 结构。
让我简要描述一下 TimeStampReq 和 TimeStampResp 结构。请求的有趣字段是:
“消息印记”,即要加盖时间戳的数据的哈希值 用于创建消息印记的哈希算法的 OID 一个可选的“nonce”,它是一个客户端选择的标识符,用于验证响应是专门为这个请求生成的。这实际上只是一种盐,用于避免重放攻击和检测错误。响应的内容是 CMS SignedData 结构。此结构中的字段包括:
用于签署响应的证书 包含TSTInfo 结构的EncapsulatedContentInfo 成员。这个结构,重要的是,包含: 请求中发送的消息印记 请求中发送的随机数 TSA 认证的时间 一组SignerInfo 结构,通常只有一个结构。对于每个 SignerInfo,结构中有趣的字段是: “签名属性”序列。这个序列的 DER 编码的 BLOB 是实际签名的。这些属性包括: TSA 认证的时间(再次) TSTInfo 结构的 DER 编码 BLOB 的哈希 颁发者和序列号或主题密钥标识符,用于从 SignedData 结构中找到的证书集中标识签名者的证书 签名本身验证时间戳的基本流程如下:
读取带有时间戳的数据,并使用时间戳请求中使用的相同哈希算法重新计算消息印记。 读取时间戳请求中使用的随机数,为此必须与时间戳一起存储。 读取并解析 TimeStampResp 结构。 验证 TSTInfo 结构是否包含正确的消息印记和随机数。 从 TimeStampResp 读取证书。 对于每个 SignerInfo: 找到该签名者的证书(应该只有一个)。 验证证书。 使用该证书,验证签名者的签名。 验证签名属性是否包含正确的 TSTInfo 结构哈希如果一切正常,那么我们知道所有签名的属性都是有效的,因为它们是签名的,并且由于这些属性包含 TSTInfo 结构的哈希,那么我们也知道这没关系。因此,我们验证了时间戳数据自 TSA 给出的时间以来没有发生变化。
因为签名数据是 DER 编码的 BLOB(其中包含不同 DER 编码的 BLOB 的散列,其中包含验证者实际关心的信息),所以在客户端(验证者)上拥有某种库是无法避免的了解 X.690 编码和 ASN.1 类型。因此,我承认将 Bouncy Castle 包含在客户端以及构建过程中,因为我没有时间自己实施这些标准。
我添加和验证时间戳的代码类似于以下内容:
构建时生成时间戳:
// a lot of fully-qualified type names here to make sure it's clear what I'm using
static void WriteTimestampToBuild()
var dataToTimestamp = ... // see OP
var hashToTimestamp = ... // see OP
var nonce = ... // see OP
var tsq = GetTimestampRequest(hashToTimestamp, nonce);
var tsr = GetTimestampResponse(tsq, "http://some.rfc3161-compliant.server");
ValidateTimestamp(tsq, tsr);
WriteToBuild("tsq-hashalg", Encoding.UTF8.GetBytes("SHA1"));
WriteToBuild("nonce", nonce.ToByteArray());
WriteToBuild("timestamp", tsr.GetEncoded());
static Org.BouncyCastle.Tsp.TimeStampRequest GetTimestampRequest(byte[] hash, Org.BouncyCastle.Math.BigInteger nonce)
var reqgen = new TimeStampRequestGenerator();
reqgen.SetCertReq(true);
return reqgen.Generate(TspAlgorithms.Sha1/*assumption*/, hash, nonce);
static void GetTimestampResponse(Org.BouncyCastle.Tsp.TimeStampRequest tsq, string url)
// similar to OP
static void ValidateTimestamp(Org.BouncyCastle.Tsp.TimeStampRequest tsq, Org.BouncyCastle.Tsp.TimeStampResponse tsr)
// same as client code, see below
static void WriteToBuild(string key, byte[] value)
// not shown
运行时的时间戳验证(客户端):
/* Just like in the OP, I've used fully-qualified names here to avoid confusion.
* In my real code, I'm not doing that, for readability's sake.
*/
static DateTime GetTimestamp()
var timestampedData = ReadFromBuild("timestamped-data");
var hashAlg = Encoding.UTF8.GetString(ReadFromBuild("tsq-hashalg"));
var timestampedHash = System.Security.Cryptography.HashAlgorithm.Create(hashAlg).ComputeHash(timestampedData);
var nonce = new Org.BouncyCastle.Math.BigInteger(ReadFromBuild("nonce"));
var tsq = new Org.BouncyCastle.Tsp.TimeStampRequestGenerator().Generate(System.Security.Cryptography.CryptoConfig.MapNameToOID(hashAlg), timestampedHash, nonce);
var tsr = new Org.BouncyCastle.Tsp.TimeStampResponse(ReadFromBuild("timestamp"));
ValidateTimestamp(tsq, tsr);
// if we got here, the timestamp is okay, so we can trust the time it alleges
return tsr.TimeStampToken.TimeStampInfo.GenTime;
static void ValidateTimestamp(Org.BouncyCastle.Tsp.TimeStampRequest tsq, Org.BouncyCastle.Tsp.TimeStampResponse tsr)
/* This compares the nonce and message imprint and whatnot in the TSTInfo.
* It throws an exception if they don't match. This doesn't validate the
* certs or signatures, though. We still have to do that in order to trust
* this data.
*/
tsr.Validate(tsq);
var tst = tsr.TimeStampToken;
var timestamp = tst.TimeStampInfo.GenTime;
var signers = tst.ToCmsSignedData().GetSignerInfos().GetSigners().Cast<Org.BouncyCastle.Cms.SignerInformation>();
var certs = tst.GetCertificates("Collection");
foreach(var signer in signers)
var signerCerts = certs.GetMatches(signer.SignerID).Cast<Org.BouncyCastle.X509.X509Certificate>().ToList();
if(signerCerts.Count != 1)
throw new Exception("Expected exactly one certificate for each signer in the timestamp");
if(!signerCerts[0].IsValid(timestamp))
/* IsValid only checks whether the given time is within the certificate's
* validity period. It doesn't verify that it's a valid certificate or
* that it hasn't been revoked. It would probably be better to do that
* kind of thing, just like I'm doing for the signing certificate itself.
* What's more, I'm not sure it's a good idea to trust the timestamp given
* by the TSA to verify the validity of the TSA's certificate. If the
* TSA's certificate is compromised, then an unauthorized third party could
* generate a TimeStampResp with any timestamp they wanted. But this is a
* chicken-and-egg scenario that my brain is now too tired to keep thinking
* about.
*/
throw new Exception("The timestamp authority's certificate is expired or not yet valid.");
if(!signer.Verify(signerCerts[0])) // might throw an exception, might not ... depends on what's wrong
/* I'm pretty sure that signer.Verify verifies the signature and that the
* signed attributes contains a hash of the TSTInfo. It also does some
* stuff that I didn't identify in my list above.
* Some verification errors cause it to throw an exception, some just
* cause it to return false. If it throws an exception, that's great,
* because that's what I'm counting on. If it returns false, let's
* throw an exception of our own.
*/
throw new Exception("Invalid signature");
static byte[] ReadFromBuild(string key)
// not shown
【讨论】:
目前我在 timestamp.verisign.com/scripts/timstamp.dll 使用 VeriSign。不过,任何符合 RFC 3161 的服务器都应该可以工作。【参考方案2】:我不确定您为什么要重建响应中签名的数据结构。实际上,如果您想从时间戳服务器响应中提取签名数据,您可以这样做:
var tsr = GetTimestamp(hashToTimestamp, nonce, "http://some.rfc3161-compliant.server");
var tst = tsr.TimeStampToken;
var tsi = tst.TimeStampInfo;
var signature = // Get the signature
var certificate = // Get the signer certificate
var signedData = tsi.GetEncoded(); // Similar to tsi.TstInfo.GetEncoded();
VerifySignature(signedData, signature, certificate)
如果要重建数据结构,则需要创建一个新的Org.BouncyCastle.Asn1.Tsp.TstInfo
实例(tsi.TstInfo
是一个Org.BouncyCastle.Asn1.Tsp.TstInfo
对象),其中所有元素都包含在响应中。
在 RFC 3161 中,签名数据结构被定义为这个 ASN.1 序列:
TSTInfo ::= SEQUENCE
version INTEGER v1(1) ,
policy TSAPolicyId,
messageImprint MessageImprint,
-- MUST have the same value as the similar field in
-- TimeStampReq
serialNumber INTEGER,
-- Time-Stamping users MUST be ready to accommodate integers
-- up to 160 bits.
genTime GeneralizedTime,
accuracy Accuracy OPTIONAL,
ordering BOOLEAN DEFAULT FALSE,
nonce INTEGER OPTIONAL,
-- MUST be present if the similar field was present
-- in TimeStampReq. In that case it MUST have the same value.
tsa [0] GeneralName OPTIONAL,
extensions [1] IMPLICIT Extensions OPTIONAL
【讨论】:
【参考方案3】:恭喜你完成了这项棘手的协议工作!
另请参阅rfc3161ng 2.0.4 上的 Python 客户端实现。
请注意,使用 RFC 3161 TSP 协议(如 Web Science and Digital Libraries Research Group: 2017-04-20: Trusted Timestamping of Mementos 和其他出版物中所述),您和您的依赖方必须相信时间戳授权 (TSA) 的运行正确且安全。当然,要真正保护大多数 TSA 运行的在线服务器,即使不是不可能,也是非常困难的。
正如该论文中所讨论的那样,通过与 TSP 的比较,现在世界上有各种信任分布和(有时)仔细监控的公共区块链,有新的可信时间戳选项(提供“存在证明”用于文件)。例如见 OriginStamp - Trusted Timestamping with Bitcoin。该协议要简单得多,并且它们为多种语言提供客户端代码。虽然他们的在线服务器也可能受到威胁,但客户端可以检查他们的哈希是否正确嵌入到比特币区块链中,从而绕过信任 OriginStamp 服务本身的需要。 一个缺点是时间戳每天只发布一次,除非支付额外费用。比特币交易变得相当昂贵,因此该服务正在考虑支持其他区块链,以降低成本并降低获得更及时发布的成本。
更新:查看 Stellar 和 Keybase 如需免费、高效、闪电般快速且经过广泛审查的时间戳,请查看 Stellar 区块链协议和STELLARAPI.IO 服务。
【讨论】:
90 年代有一个电子邮件服务在做时间戳。它 PGP 签署了消息加上链的状态,并使用这些签名更新了链。它将签名(或链的头部)发布到 Usenet。今天的区块链非常昂贵,需要添加工作证明。 @JeffreyGoldberg 看到我对免费“stallarapi.io”区块链替代方案的回答的更新,它不是工作量证明。我用过你说的邮件服务:stamper,还在运行:itconsult.co.uk/stamper.htm以上是关于验证 RFC 3161 可信时间戳的主要内容,如果未能解决你的问题,请参考以下文章
在 Python 中生成 RFC 3339 时间戳 [重复]
如何在 Swift 中解析/创建格式为小数秒 UTC 时区(ISO 8601、RFC 3339)的日期时间戳?