Статьи

Использование надежной метки времени с Java

Доверенная отметка времени — это процесс, когда доверенная третья сторона (« Орган отметки времени», TSA) сертифицирует время данного события в электронной форме. Регламент ЕС eIDAS дает этим временным отметкам юридическую силу — т.е. никто не может оспаривать время или содержание события, если оно было отмечено. Это применимо к нескольким сценариям, включая временные метки журналов аудита. (Примечание: временная метка недостаточна для хорошего контрольного журнала, так как она не мешает злоумышленнику полностью удалить событие)

Существует ряд стандартов для надежной метки времени, основным из которых является RFC 3161 . Как и большинство RFC, это трудно читать. К счастью для пользователей Java, BouncyCastle реализует стандарт. К сожалению, как и в большинстве API безопасности, работать с ним сложно, даже ужасно. Я должен был реализовать это, поэтому я поделюсь кодом, необходимым для отметки времени данных.

Всю суть можно найти здесь , но я постараюсь объяснить основной поток. Очевидно, что существует много кода, который просто следует стандарту. Классы BouncyCastle — это лабиринт, по которому трудно ориентироваться.

Основным методом, очевидно, является timestamp(hash, tsaURL, username, password, tsaPolicyOid) :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public TimestampResponseDto timestamp(byte[] hash, String tsaUrl, String tsaUsername, String tsaPassword, String tsaPolicyOid) throws IOException {
    MessageImprint imprint = new MessageImprint(sha512oid, hash);
 
    ASN1ObjectIdentifier tsaPolicyId = StringUtils.isNotBlank(tsaPolicyOid) ? new ASN1ObjectIdentifier(tsaPolicyOid) : baseTsaPolicyId;
 
    TimeStampReq request = new TimeStampReq(imprint, tsaPolicyOid, new ASN1Integer(random.nextLong()),
            ASN1Boolean.TRUE, null);
 
    byte[] body = request.getEncoded();
    try {
        byte[] responseBytes = getTSAResponse(body, tsaUrl, tsaUsername, tsaPassword);
 
        ASN1StreamParser asn1Sp = new ASN1StreamParser(responseBytes);
        TimeStampResp tspResp = TimeStampResp.getInstance(asn1Sp.readObject());
        TimeStampResponse tsr = new TimeStampResponse(tspResp);
 
        checkForErrors(tsaUrl, tsr);
 
        // validate communication level attributes (RFC 3161 PKIStatus)
        tsr.validate(new TimeStampRequest(request));
 
        TimeStampToken token = tsr.getTimeStampToken();
             
        TimestampResponseDto response = new TimestampResponseDto();
        response.setTime(getSigningTime(token.getSignedAttributes()));
        response.setEncodedToken(Base64.getEncoder().encodeToString(token.getEncoded()));
            
        return response;
    } catch (RestClientException | TSPException | CMSException | OperatorCreationException | GeneralSecurityException e) {
        throw new IOException(e);
    }
}

Он готовит запрос, создавая отпечаток сообщения. Обратите внимание, что вы передаете сам хэш, а также алгоритм хеширования, используемый для его создания. Почему API не скрывает это от вас, я не знаю. В моем случае хеш получается более сложным способом, поэтому он полезен, но все же. Затем мы получаем необработанную форму запроса и отправляем ее в TSA (орган отметки времени). Это HTTP-запрос, довольно простой, но вы должны позаботиться о некоторых заголовках запросов и ответов, которые не обязательно согласованы между TSA. Имя пользователя и пароль являются необязательными, некоторые TSA предлагают услугу (с ограниченной скоростью) без аутентификации. Также обратите внимание на tsaPolicyOid — большинство TSA имеют свою особую политику, которая задокументирована на их странице, и вы должны получить оттуда OID.

Когда вы получаете необработанный ответ, вы анализируете его в TimeStampResponse. Опять же, вам нужно пройти через 2 промежуточных объекта (ASN1StreamParser и TimeStampResp), которые могут быть правильной абстракцией, но не пригодным для использования API.

Затем вы проверяете, был ли ответ успешным, и вам также нужно проверить его — TSA, возможно, вернул неверный ответ. В идеале все это могло бы быть скрыто от вас. Валидация выдает исключение, которое в этом случае я просто распространяю, заключая в IOException.

Наконец, вы получаете токен и возвращаете ответ. Самое главное — это содержимое токена, который в моем случае был нужен как Base64, поэтому я его кодирую. Это могут быть и необработанные байты. Если вы хотите получить какие-либо дополнительные данные из токена (например, время подписания), это не так просто; Вы должны проанализировать атрибуты низкого уровня (видно в сущности).

Хорошо, теперь у вас есть токен, и вы можете сохранить его в базе данных. Иногда вы можете захотеть проверить, не были ли помечены временные метки (это мой вариант использования). Код здесь , и я даже не буду пытаться объяснить его — это тонна шаблонной модели, которая также учитывает различия в реакции TSA (я пробовал несколько). Тот факт, что нужен класс DummyCertificate, либо означает, что я что-то неправильно понял, либо подтверждает мою критику в отношении API BouncyCastle. DummyCertificate может не понадобиться для некоторых TSA, но для других, и вы на самом деле не можете создать его так легко. Вам нужен реальный сертификат для его создания (который не включен в гистолог; с помощью метода init () в следующем гисте вы можете создать манекен с помощью dummyCertificate = new DummyCertificate(certificateHolder.toASN1Structure()); ). В моем коде это все один класс, но для их представления я решил разделить его, отсюда и небольшое дублирование.

Хорошо, теперь мы можем ставить отметки времени и проверять отметки времени. Этого должно быть достаточно; но для целей тестирования (или ограниченного внутреннего использования) вы можете захотеть делать временную отметку локально, а не запрашивать TSA. Код можно найти здесь . Он использует Spring, но вместо этого вы можете передать детали хранилища ключей в качестве аргументов методу init. Вам нужно JKS-хранилище с парой ключей и сертификатом, и я использовал KeyStore Explorer для их создания . Если вы запускаете свое приложение в AWS, вы можете зашифровать хранилище ключей с помощью KMS (службы управления ключами), а затем расшифровать его при загрузке приложения, но это выходит за рамки этой статьи. Поскольку локальная отметка времени работает как положено, а отметка времени — вместо вызова внешней службы, просто вызовите localTSA.timestamp(req);

Как я узнал, какие классы создавать и какие параметры передавать — я не помню. Глядя на тесты, примеры, ответы, источники. Потребовалось некоторое время, и поэтому я делюсь этим, чтобы потенциально спасти некоторые проблемы других.

Список TSA, с которыми вы можете протестировать: SafeCreative , FreeTSA , time.centum.pl .

Я понимаю, что это не относится ко многим сценариям, но я бы порекомендовал отметить некоторые критические фрагменты данных вашего приложения. И, как правило, полезно иметь его в своем «наборе инструментов», готовым к использованию, вместо того, чтобы пытаться читать стандарт и бороться с классами BouncyCastle в течение нескольких дней, чтобы выполнить эту якобы простую задачу.

Опубликовано на Java Code Geeks с разрешения Божидара Божанова, партнера нашей программы JCG . Посмотрите оригинальную статью здесь: Использование надежной метки времени с Java

Мнения, высказанные участниками Java Code Geeks, являются их собственными.