验证 RFC 3161 受信任的时间戳



在我的构建过程中,我想包含来自符合 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函数。

我终于自己想通了。 这应该不足为奇,但答案是令人作呕的复杂和间接的。

拼图缺失的部分在 RFC 5652 中。 直到我阅读(好吧,浏览)该文档,我才真正理解TimeStampResp结构。

让我简要描述一下 TimeStampReq 和 TimeStampResp 结构。 请求的有趣字段是:

  • "消息印记",即要加盖时间戳的数据的哈希
  • 用于创建消息印记的哈希算法的 OID
  • 可选的"nonce",它是客户端选择的标识符,用于验证响应是否专门针对此请求生成。 这实际上只是一种盐,用于避免重放攻击和检测错误。

响应的实质是 CMS SignedData 结构。 此结构中的字段包括:

  • 用于对响应进行签名的证书
  • 包含 TSTInfo 结构的 EncapsulatedContentInfo 成员。 重要的是,此结构包含:
    • 请求中发送的消息印记
    • 请求中发送的随机数
    • TSA 认证的时间
  • 一组 SignerInfo 结构,通常集合中只有一个结构。 对于每个签名者信息,结构中感兴趣的字段包括:
    • 一系列"签名属性"。 此序列的 DER 编码 BLOB 是实际签名的内容。 这些属性包括:
      • TSA 认证的时间(再次)
      • TSTInfo 结构的 DER 编码 BLOB 的哈希
    • 颁发者和序列号或使用者密钥标识符,用于从 SignedData 结构中找到的证书集中标识签名者的证书
    • 签名本身

验证时间戳的基本过程如下:

  • 读取带有时间戳的数据,并使用时间戳请求中使用的相同哈希算法重新计算消息印记。
  • 读取时间戳请求中使用的随机数,为此目的,必须将其与时间戳一起存储。
  • 读取并解析 TimeStampResp 结构。
  • 验证 TSTInfo 结构是否包含正确的消息版本说明和随机数。
  • 从时间戳中,读取证书。
  • 对于每个签名者信息:
    • 查找该签名者的证书(应该只有一个)。
    • 验证证书。
    • 使用该证书验证签名者的签名。
    • 验证签名属性是否包含 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
}
<</div> div class="one_answers">

我不确定为什么要重建在响应中签名的数据结构。 实际上,如果要从时间戳服务器响应中提取签名数据,则可以这样做:

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  }

恭喜你完成了这个棘手的协议工作!

另请参阅 rfc3161ng 2.0.4 中的 Python 客户端实现。

请注意,使用 RFC 3161 TSP 协议,如 Web 科学和数字图书馆研究组:2017-04-20:纪念品和其他出版物的可信时间戳中所述,您和您的信赖方必须相信时间戳颁发机构 (TSA) 已正确且安全地运行。当然,要真正保护像大多数TSA运行的在线服务器一样,即使不是不可能,也是非常困难的。

正如该论文所讨论的,与TSP进行比较,现在世界上有各种各样的公共区块链,其中信任被分发和(有时)仔细监控,有了新的可信时间戳选项(为文档提供"存在证明")。 例如,请参阅OriginStamp - 比特币的可信时间戳。该协议要简单得多,它们为多种语言提供客户端代码。虽然他们的在线服务器也可能受到损害,但客户可以检查他们的哈希值是否正确嵌入到比特币区块链中,从而绕过信任OriginStamp服务本身的需要。一个缺点是时间戳每天只发布一次,除非进行额外付款。 比特币交易已经变得相当昂贵,因此该服务正在考虑支持其他区块链,以降低成本,并使其更便宜地获得更及时的发布。

更新:查看恒星和密钥库有关免费,高效,闪电般快速,经过广泛审查的时间戳,请查看Stellar区块链协议和STELLARAPI。IO 服务。

相关内容

  • 没有找到相关文章

最新更新