验证RFC 3161可信时间戳 [英] Verify RFC 3161 trusted timestamp

查看:426
本文介绍了验证RFC 3161可信时间戳的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的构建过程中,我想包含一个符合RFC-3161的TSA的时间戳.在运行时,代码将验证此时间戳​​,最好在没有第三方库帮助的情况下. (这是一个.NET应用程序,因此我可以随时使用标准的哈希和非对称密码功能.)

In my build process, I want to include a timestamp from an RFC-3161-compliant TSA. At run time, the code will verify this timestamp, preferably without the assistance of a third-party library. (This is a .NET application, so I have standard hash and asymmetric cryptography functionality readily at my disposal.)

依赖于ASN.1和X.690等的RFC 3161实施起来并不容易,因此至少现在,我正在使用Bouncy Castle生成TimeStampReq(请求)并解析TimeStampResp(回复).我只是不太清楚如何验证响应.

RFC 3161, with its reliance on ASN.1 and X.690 and whatnot, is not simple to implement, so for now at least, I'm using Bouncy Castle to generate the TimeStampReq (request) and parse the TimeStampResp (response). I just can't quite figure out how to validate the response.

到目前为止,我已经弄清楚了如何提取签名本身,公共证书,创建时间戳的时间以及我发送的消息烙印摘要和随机数(用于构建时验证).我不知道如何将这些数据放在一起以生成经过散列和签名的数据.

So far, I've figured out how to extract the signature itself, the public cert, the time the timestamp was created, and the message imprint digest and nonce that I sent (for build-time validation). What I can't figure out is how to put this data together to generate the data that was hashed and signed.

这是我正在做的事情和我正在尝试做的事情的粗略概念.这是测试代码,因此我采取了一些捷径.我必须清理几件事情,并在得到可行的东西后以正确的方式进行处理.

Here's a rough idea of what I'm doing and what I'm trying to do. This is test code, so I've taken some shortcuts. I'll have to clean a couple of things up and do them the right way once I get something that works.

// 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函数.

As you might guess, where I think I'm stuck is the MagicallyCombineThisStuff function.

推荐答案

我终于自己弄清楚了.不足为奇,但是答案令人作呕的复杂和间接.

I finally figured it out myself. It should come as no surprise, but the answer is nauseatingly complex and indirect.

这个难题遗漏的部分是RFC5652.在我阅读(很好地浏览)该文档之前,我并不真正了解TimeStampResp的结构.

The missing pieces to the puzzle were in RFC 5652. I didn't really understand the TimeStampResp structure until I read (well, skimmed through) that document.

让我简要介绍一下TimeStampReq和TimeStampResp结构.请求的有趣字段是:

Let me describe in brief the TimeStampReq and TimeStampResp structures. The interesting fields of the request are:

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

响应的内容是CMS SignedData 结构.此结构中的字段包括:

The meat of the response is a CMS SignedData structure. Among the fields in this structure are:

  • 用于签署回复的证书
  • 包含 TSTInfo 结构的EncapsulatedContentInfo成员.重要的是,此结构包含:
    • 请求中发送的消息烙印
    • 请求中发送的随机数
    • TSA认证的时间
    • the certificate(s) used to sign the response
    • an EncapsulatedContentInfo member containing a TSTInfo structure. This structure, importantly, contains:
      • the message imprint that was sent in the request
      • the nonce that was sent in the request
      • the time certified by the TSA
      • 一系列签名属性".此序列的DER编码的BLOB是实际签名的.这些属性包括:
        • 再次获得TSA认证的时间
        • TSTInfo结构的DER编码的BLOB的哈希
        • a sequence of "signed attributes". The DER-encoded BLOB of this sequence is what is actually signed. Among these attributes are:
          • the time certified by the TSA (again)
          • a hash of the DER-encoded BLOB of the TSTInfo structure

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

          The basic process of validating the timestamp is as follows:

          • 读取带有时间戳的数据,并使用时间戳请求中使用的相同散列算法重新计算消息印记.
          • 读取时间戳请求中使用的随机数,为此,它必须与时间戳一起存储.
          • 读取并解析TimeStampResp结构.
          • 验证TSTInfo结构是否包含正确的消息标记和随机数.
          • 从TimeStampResp中读取证书.
          • 对于每个SignerInfo:
            • 找到该签名者的证书(应该恰好是一个).
            • 验证证书.
            • 使用该证书,验证签名者的签名.
            • 验证已签名的属性是否包含TSTInfo结构的正确哈希值
            • Read the data that was timestamped, and recompute the message imprint using the same hashing algorithm used in the timestamp request.
            • Read the nonce used in the timestamp request, which must be stored along with the timestamp for this purpose.
            • Read and parse the TimeStampResp structure.
            • Verify that the TSTInfo structure contains the correct message imprint and nonce.
            • From the TimeStampResp, read the certificate(s).
            • For each SignerInfo:
              • Find the certificate for that signer (there should be exactly one).
              • Verify the certificate.
              • Using that certificate, verify the signer's signature.
              • Verify that the signed attributes contain the correct hash of the TSTInfo structure

              如果一切正常,那么我们知道所有已签名的属性都是有效的,因为它们已经被签名,并且由于这些属性包含TSTInfo结构的哈希,因此我们也知道这也是可以的.因此,我们已经验证了带有时间戳的数据自TSA给出的时间以来一直没有改变.

              If everything is okay, then we know that all signed attributes are valid, since they're signed, and since those attributes contain a hash of the TSTInfo structure, then we know that's okay, too. We have therefore validated that the timestamped data is unchanged since the time given by the TSA.

              因为签名的数据是DER编码的BLOB(其中包含不同DER编码的BLOB的散列,其中包含验证者实际关心的信息),所以在客户端(验证者)拥有某种类型的库是无处可逃的了解X.690编码和ASN.1类型.因此,我同意在客户和构建过程中加入Bouncy Castle,因为我没有时间自己来实施这些标准.

              Because the signed data is a DER-encoded BLOB (which contains a hash of the different DER-encoded BLOB containing the information the verifier actually cares about), there's no getting around having some sort of library on the client (verifier) that understands X.690 encoding and ASN.1 types. Therefore, I conceded to including Bouncy Castle in the client as well as in the build process, since there's no way I have time to implement those standards myself.

              我添加和验证时间戳的代码类似于以下内容:

              My code to add and verify timestamps is similar to the following:

              // 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
              }
              

              这篇关于验证RFC 3161可信时间戳的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆