Verify RFC 3161 trusted timestamp

那年仲夏 提交于 2019-11-28 17:19:26

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

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.

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

  • a "message imprint", which is the hash of the data to be timestamped
  • the OID of the hash algorithm used to create the message imprint
  • an optional "nonce", which is a client-chosen identifier used to verify that the response is generated specifically for this request. This is effectively just a salt, used to avoid replay attacks and to detect errors.

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

  • 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
  • a set of SignerInfo structures, with typically just one structure in the set. For each SignerInfo, the interesting fields within the structure are:
    • 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
    • an issuer and serial number or subject key identifier that identifies the signer's certificate from the set of certificates found in the SignedData structure
    • the signature itself

The basic process of validating the timestamp is as follows:

  • 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

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.

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:

Timestamp generation at build time:

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

Timestamp verification at run time (client site):

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

I am not sure to understand why you want to rebuild the data structure signed in the response. Actually if you want to extract the signed data from the time-stamp server response you can do this:

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)

If you want to rebuild the data structure, you need to create a new Org.BouncyCastle.Asn1.Tsp.TstInfo instance (tsi.TstInfo is a Org.BouncyCastle.Asn1.Tsp.TstInfo object) with all elements contained in the response.

In RFC 3161 the signed data structure is defined as this ASN.1 sequence:

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  }

Congratulations on getting that tricky protocol work done!

See also a Python client implementation at rfc3161ng 2.0.4.

Note that with the RFC 3161 TSP protocol, as discussed at Web Science and Digital Libraries Research Group: 2017-04-20: Trusted Timestamping of Mementos and other publications, you and your relying parties must trust that the Time-Stamping Authority (TSA) is operated properly and securely. It is of course very difficult, if not impossible, to really secure online servers like those run by most TSAs.

As also discussed in that paper, with comparisons to TSP, now that the world has a variety of public blockchains in which trust is distributed and (sometimes) carefully monitored, there are new trusted timestamping options (providing "proof of existence" for documents). For example see OriginStamp - Trusted Timestamping with Bitcoin. The protocol is much much simpler, and they provide client code for a large variety of languages. While their online server could also be compromised, the client can check whether their hashes were properly embedded in the Bitcoin blockchain and thus bypass the need to trust the OriginStamp service itself. One downside is that timestamps are only posted once a day, unless an extra payment is made. Bitcoin transactions have become rather expensive, so the service is looking at supporting other blockchains also to drive costs back down and make it cheaper to get more timely postings.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!