Я хочу подписать данные с помощью удалённого подписанта.

Мы рекомендуем использовать примитивы Prehash и SignPrehash с ключом ML_DSA_65, если закрытый ключ хранится в месте, откуда сообщение не может быть отправлено.

Иногда сторона, владеющая ключом подписи, не может — или не должна — получать сообщение сама: ключ хранится в HSM или KMS, или размер сообщения превышает лимит размера запроса, установленный для подписывающей стороны.

Примитивы Prehash и SignPrehash решают эту проблему, разделяя процесс подписания на два этапа. Вычисляется короткое значение прехеша в месте расположения сообщения, используя только открытый ключ, и отправляется подписывающему лицу. Подписывающее лицо преобразует его в подпись, используя закрытый ключ, не видя самого сообщения. В режиме External Mu (алгоритм, поддерживаемый Tink) значение прехеша составляет 69 байт, независимо от размера сообщения.

В результате вы получаете обычную подпись, продублированную над исходным сообщением: проверяющие используют стандартный примитив цифровой подписи и им не нужно знать, что были задействованы два этапа.

Прежде чем начать

Создайте набор ключей ML-DSA, ключи которого требуют указания идентификатора — либо варианта TINK , либо NO_PREFIX_WITH_PREHASH_ID если вы не хотите, чтобы в результирующей подписи присутствовал префикс. Предоставьте подписывающей стороне закрытый набор ключей, а стороне, выполняющей предварительное хеширование, — соответствующий открытый набор ключей. У подписывающей стороны должен быть включенный ключ для каждого идентификатора ключа, который может сгенерировать сторона, выполняющая предварительное хеширование; см. раздел «Наборы ключей» .

Шаг 1: Вычислите значение прехеша.

Запустите этот код там, где находится сообщение. Для его работы требуется только открытый набор ключей.

C++

#include "tink/keyset_handle.h"
#include "tink/signature/config_2026.h"
#include "tink/signature/prehash.h"

absl::StatusOr<std::unique_ptr<crypto::tink::Prehash>> prehasher =
    public_handle.GetPrimitive<crypto::tink::Prehash>(
        crypto::tink::ConfigSignature2026());
if (!prehasher.ok()) return prehasher.status();

absl::StatusOr<std::string> prehash = (*prehasher)->Compute(message);
if (!prehash.ok()) return prehash.status();

Идти

import "github.com/tink-crypto/tink-go/v2/signprehash"

prehasher, err := signprehash.NewPrehash(publicHandle)
if err != nil {
    return err
}
prehash, err := prehasher.ComputePrehash(message)
if err != nil {
    return err
}

Шаг 2: Отправьте значение прехеша подписывающему лицу.

Отправьте значение прехеша тому, кто хранит закрытый ключ. Оно не является секретом, но необходимо защитить его целостность при передаче: злоумышленник, способный изменить его в процессе передачи, контролирует то, что будет подписано.

Шаг 3: Подпишите значение прехеша.

Запустите этот код там, где находится закрытый ключ.

C++

#include "tink/keyset_handle.h"
#include "tink/signature/config_2026.h"
#include "tink/signature/sign_prehash.h"

absl::StatusOr<std::unique_ptr<crypto::tink::SignPrehash>> signer =
    private_handle.GetPrimitive<crypto::tink::SignPrehash>(
        crypto::tink::ConfigSignature2026());
if (!signer.ok()) return signer.status();

absl::StatusOr<std::string> signature = (*signer)->Sign(prehash);
if (!signature.ok()) return signature.status();

Идти

import "github.com/tink-crypto/tink-go/v2/signprehash"

signer, err := signprehash.NewPrehashSigner(privateHandle)
if err != nil {
    return err
}
sig, err := signer.SignPrehash(prehash)
if err != nil {
    return err
}

Шаг 4: Проверка подписи

Проверка — это обычный процесс цифровой подписи исходного сообщения , а не его прехеш-значения.

C++

absl::StatusOr<std::unique_ptr<crypto::tink::PublicKeyVerify>> verifier =
    public_handle.GetPrimitive<crypto::tink::PublicKeyVerify>(
        crypto::tink::ConfigSignature2026());
if (!verifier.ok()) return verifier.status();

absl::Status verified = (*verifier)->Verify(signature, message);

Идти

import "github.com/tink-crypto/tink-go/v2/signature"

verifier, err := signature.NewVerifier(publicHandle)
if err != nil {
    return err
}
if err := verifier.Verify(sig, message); err != nil {
    return err
}

Прехеш и Сигпрехеш

Примитивы Prehash и SignPrehash разделяют вычисление цифровой подписи на два этапа:

  1. Для работы функции prehash требуется только открытый ключ. Она преобразует сообщение произвольной длины в короткое значение prehash фиксированного размера.
  2. Для работы функции SignPrehash необходим закрытый ключ. Она преобразует значение прехеша в подпись.

Полученная подпись представляет собой обычную подпись над исходным сообщением. Вы проверяете её с помощью стандартного примитива Digital Signature PublicKeyVerify , и проверяющим не нужно знать — или интересоваться — тем, что подпись была создана в два этапа.

Рассматривайте значение прехеша как непрозрачные байты. Оно имеет фиксированный размер и содержит идентификатор ключа, для которого было вычислено, но его структура является частью формата передачи данных Tink, и вам не следует самостоятельно его анализировать или создавать. Если вы занимаетесь портированием Tink или вам необходимы подробности на уровне байтов, см. формат передачи данных Tink .

Используйте эту пару примитивов, когда:

  • Ключ подписи хранится в другом месте , например, в HSM, в KMS или за границей RPC, и вам не нужно отправлять полное сообщение через эту границу.
  • Сообщение большое , и удалённый подписант устанавливает ограничение на размер запроса.

Если ни один из этих вариантов не подходит, используйте вместо этого простой примитив цифровой подписи : он проще и его сложнее использовать не по назначению.

Наборы клавиш

Эти два примитива выбирают ключи по-разному, аналогично тому, как это происходит при подписании и проверке для примитива цифровой подписи :

  • Prehash.Compute всегда использует первичный ключ из открытого набора ключей и записывает идентификатор этого ключа в значение prehash. Именно она выбирает , с каким ключом будет создана подпись.
  • SignPrehash.Sign считывает идентификатор ключа из значения прехеша и подписывает с помощью соответствующего включенного ключа из закрытого набора ключей. Это та сторона, которая следует выбору, уже сделанному кем-то другим. Если ни один из включенных ключей в наборе ключей не имеет этого идентификатора, вызов завершается неудачей.

Каждый ключ в наборе ключей SignPrehash должен иметь идентификатор (ID), иначе создание примитива завершится неудачей.

Минимальные гарантии безопасности

  • Полученная подпись обладает теми же свойствами, что и подпись, созданная с помощью примитива «Цифровая подпись» с тем же типом ключа.
  • Tink добавляет к значению прехеша 5 байтов, содержащих специальное зарезервированное значение и идентификатор ключа, для которого оно было вычислено. SignPrehash подписывает значение только этим ключом. Криптографически ли привязано значение к этому ключу, зависит от алгоритма; для External Mu ML-DSA это так, см. формат передачи Tink .
  • Сообщения могут иметь произвольную длину.

На что следует обратить внимание

  • Подписывающая сторона не может проверить, что именно она подписывает. Любой, кто может вызвать SignPrehash может подписать произвольное сообщение, и у подписывающей стороны нет возможности применить политику к содержимому сообщения. Защищайте доступ к SignPrehash точно так же, как вы защищаете доступ к PublicKeySign .
  • Защитите значение прехеша во время передачи. Оно не является секретом, но злоумышленник, способный изменить его в процессе передачи, контролирует то, что будет подписано.

Выберите тип ключа

ML-DSA в режиме External Mu , как описано в RFC 9881 , является единственным алгоритмом, который Tink поддерживает для Prehash и SignPrehash. Для работы с этими примитивами следует использовать стандартный ключ ML-DSA.

Для большинства случаев использования мы рекомендуем ML_DSA_65 .

Ключ должен содержать идентификатор, поскольку каждое значение прехеша начинается с префикса, содержащего идентификатор ключа, для которого оно было вычислено. Допускаются следующие варианты:

  • TINK — полученная сигнатура начинается с обычного 5-байтового префикса вывода Tink.
  • NO_PREFIX_WITH_PREHASH_ID — результирующая подпись не содержит выходного префикса, в то время как ключ по-прежнему имеет идентификатор, необходимый для значения прехеша.

Ключи, использующие вариант NO_PREFIX (в исходном виде), не поддерживаются, поскольку у них нет идентификатора ключа.