Подтвердить надежную временную метку RFC 3161
В моем процессе сборки я хочу включить метку времени из TSA, совместимого с RFC-3161. Во время выполнения код проверяет эту метку времени, желательно без помощи сторонней библиотеки. (Это приложение .NET, поэтому у меня есть стандартная хэш-функция и асимметричная криптографическая функциональность в моем распоряжении.)
RFC 3161, опираясь на ASN.1 и X.690, а также не просто реализовать, поэтому, по крайней мере, я использую Bouncy Castle для создания TimeStampReq (запроса) и разбора TimeStampResp ( ответ). Я просто не могу понять, как проверить ответ.
До сих пор я выяснял, как извлечь собственно подпись, публичный сертификат, время создания метки времени и дайджест отпечатка сообщения и сообщение, которое я отправил (для проверки времени сборки). Я не могу понять, как собрать эти данные для генерации данных, которые были хэшированы и подписаны.
Вот грубое представление о том, что я делаю и что я пытаюсь сделать. Это тестовый код, поэтому я сделал несколько ярлыков. Мне придется очистить пару вещей и сделать их правильно, как только я получу что-то, что работает.
Создание временной метки во время сборки:
// a lot of fully-qualified type names here to make sure it 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 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. Среди полей в этой структуре:
- сертификат (ы), используемый для подписи ответа
- член EncapsulatedContentInfo, содержащий структуру TSTInfo. Эта структура, что важно, содержит:
- отпечаток сообщения, отправленный в запросе
- nonce, который был отправлен в запросе
- время, сертифицированное TSA
- набор структур SignerInfo, с типично только одной структурой в наборе. Для каждого SignerInfo интересными полями в структуре являются:
- последовательность "подписанных атрибутов". BLOB этой последовательности, закодированной DER, является тем, что фактически подписано. Среди этих атрибутов:
- время, заверенное TSA (снова)
- хэш-код BLOB структуры, основанной на DER, структуры TSTInfo
- эмитент и серийный номер или идентификатор ключевого ключа, который идентифицирует сертификат подписчика из набора сертификатов, найденных в структуре SignedDatali >
- сама подпись
Основной процесс проверки метки времени выглядит следующим образом:
- Прочитайте данные, которые были отмечены по времени, и пересчитайте отпечаток сообщения, используя тот же алгоритм хэширования, который используется в запросе метки времени.
- Прочитайте значение nonce, используемое в запросе метки времени, которое необходимо сохранить вместе с меткой времени для этой цели.
- Прочитайте и проанализируйте структуру TimeStampResp.
- Убедитесь, что структура TSTInfo содержит правильный отпечаток сообщения и nonce.
- Из TimeStampResp прочитайте сертификат (ы).
- Для каждого SignerInfo:
- Найти сертификат для этого подписчика (должен быть ровно один).
- Проверить сертификат.
- Используя этот сертификат, проверьте подпись подписчика.
- Убедитесь, что подписанные атрибуты содержат правильный хэш структуры TSTInfo.
Если все в порядке, то мы знаем, что все подписанные атрибуты действительны, поскольку они подписаны, и поскольку эти атрибуты содержат хэш структуры TSTInfo, мы также знаем, что все в порядке. Поэтому мы подтвердили, что временные данные не изменяются с момента, указанного TSA.
Поскольку подписанные данные представляют собой BLOB с DER-кодированием (который содержит хэш из другого BLOB, закодированного в DER-коде, содержащий информацию, на которую действительно волнует верификатор), нет никакой возможности иметь некоторую библиотеку на клиенте (верификаторе) который понимает кодировку X.690 и типы ASN.1. Поэтому я согласился с тем, чтобы включить Bouncy Castle в клиента, а также в процесс сборки, так как у меня нет времени для реализации этих стандартов самостоятельно.
Мой код для добавления и проверки временных меток аналогичен следующему:
Создание временной метки во время сборки:
// a lot of fully-qualified type names here to make sure it 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 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 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 more, I'm not sure it a good idea to trust the timestamp given
* by the TSA to verify the validity of the TSA certificate. If the
* TSA 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 certificate is expired or not yet valid.");
}
if(!signer.Verify(signerCerts[0])){ // might throw an exception, might not ... depends on what 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 great,
* because that 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
}
Ответ 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
Поздравляем вас с тем, что эта сложная работа по протоколу сделана!
См. также реализацию клиента Python на rfc3161ng 2.0.4.
Обратите внимание, что с протоколом TSP RFC 3161, как обсуждалось в Исследовательская группа по веб-науке и цифровым библиотекам: 2017-04-20: надежная отметка времени и других публикациях, вы и ваши доверяющие стороны должны доверять правильному и безопасному управлению полномочным органом времени (TSA). Конечно, очень сложно, если не невозможно, действительно защищать онлайн-серверы, подобные тем, которые выполняются большинством TSA.
Как также обсуждается в этой статье, при сравнении с TSP, теперь, когда в мире существует множество общественных цепочек, в которых доверие распределяется и (иногда) тщательно контролируется, существуют новые надежные параметры временной привязки (предоставление "доказательства существования" ) для документов). Например, см.
OriginStamp - надежная отметка времени с помощью биткойна. Протокол намного проще, и они предоставляют клиентский код для большого количества языков. В то время как их онлайн-сервер также может быть скомпрометирован, клиент может проверить, правильно ли внедрены их хеширование в блок-цепочке биткойнов и, таким образом, обойти необходимость доверять самой службе OriginStamp.
Один недостаток заключается в том, что временные метки публикуются только один раз в день, если только не производится дополнительная оплата. Биткойн-транзакции стали довольно дорогими, поэтому служба ищет поддержки других блок-цепочек, чтобы также снизить затраты и сделать более дешевыми получать более своевременные сообщения.