18

ビルド プロセスに、RFC-3161 準拠の TSA からのタイムスタンプを含めたいと考えています。実行時に、コードはこのタイムスタンプを検証しますが、できればサードパーティ ライブラリの支援は必要ありません。(これは .NET アプリケーションなので、標準のハッシュおよび非対称暗号化機能を自由に使用できます。)

ASN.1 や X.690 などに依存する RFC 3161 の実装は簡単ではありません。そのため、少なくとも今のところ、Bouncy Castle を使用して TimeStampReq (要求) を生成し、TimeStampResp (応答) を解析しています。応答を検証する方法がよくわかりません。

これまでのところ、署名自体、公開証明書、タイムスタンプが作成された時刻、および送信したメッセージ インプリント ダイジェストと nonce (ビルド時の検証用) を抽出する方法を理解しました。私が理解できないのは、このデータをまとめて、ハッシュされ署名されたデータを生成する方法です。

これが私がやっていることと私がやろうとしていることの大まかな考えです。これはテスト コードなので、いくつかのショートカットを使用しています。うまくいくものを手に入れたら、いくつかのことを片付けて、正しい方法で行う必要があります。

ビルド時のタイムスタンプ生成:

// 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関数です。

4

3 に答える 3

21

私は最終的にそれを自分で理解しました。驚くべきことではありませんが、答えはうんざりするほど複雑で間接的です。

このパズルの欠けているピースは RFC 5652 にありました。私はその文書を読む (ざっと目を通す) まで、TimeStampResp 構造をよく理解していませんでした。

TimeStampReq および TimeStampResp 構造体について簡単に説明します。リクエストの興味深いフィールドは次のとおりです。

  • タイムスタンプが付けられるデータのハッシュである「メッセージ インプリント」
  • メッセージ インプリントの作成に使用されるハッシュ アルゴリズムの OID
  • オプションの「ノンス」。これは、応答がこの要求に対して特別に生成されたことを確認するために使用される、クライアントが選択した識別子です。これは事実上単なるソルトであり、リプレイ攻撃を回避し、エラーを検出するために使用されます。

レスポンスの中身は CMS SignedData構造です。この構造体のフィールドには次のものがあります。

  • 応答の署名に使用された証明書
  • TSTInfo構造を含む EncapsulatedContentInfo メンバー。重要なことに、この構造には以下が含まれます。
    • リクエストで送信されたメッセージの刻印
    • リクエストで送信された nonce
    • TSA認定時刻
  • SignerInfo構造体のセット。通常、セット内の構造体は 1 つだけです。SignerInfo ごとに、構造内の興味深いフィールドは次のとおりです。
    • 「署名された属性」のシーケンス。このシーケンスの DER でエンコードされた BLOB は、実際に署名されているものです。これらの属性には次のものがあります。
      • TSAによって認定された時間(再び)
      • TSTInfo 構造体の DER でエンコードされた BLOB のハッシュ
    • SignedData 構造で見つかった一連の証明書から署名者の証明書を識別する、発行者とシリアル番号またはサブジェクト キー識別子
    • 署名自体

タイムスタンプを検証する基本的なプロセスは次のとおりです。

  • タイムスタンプが付けられたデータを読み取り、タイムスタンプ リクエストで使用されたのと同じハッシュ アルゴリズムを使用して、メッセージ インプリントを再計算します。
  • タイムスタンプ リクエストで使用される nonce を読み取ります。これは、この目的のためにタイムスタンプと共に保存する必要があります。
  • TimeStampResp 構造を読み取って解析します。
  • TSTInfo 構造体に正しいメッセージ インプリントと nonce が含まれていることを確認します。
  • TimeStampResp から、証明書を読み取ります。
  • SignerInfo ごとに:
    • その署名者の証明書を見つけます (正確に 1 つあるはずです)。
    • 証明書を確認します。
    • その証明書を使用して、署名者の署名を確認します。
    • 署名された属性に 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
}
于 2013-10-30T23:43:52.327 に答える