Quiero firmar datos con un firmante remoto

Recomendamos las primitivas Prehash y SignPrehash con una clave ML_DSA_65 cuando la clave privada se encuentra en un lugar desde el que no se puede enviar el mensaje.

A veces, la parte que tiene la clave de firma no puede (o no debería) recibir el mensaje en sí: la clave reside en un HSM o un KMS, o el mensaje es más grande que el límite de tamaño de la solicitud del firmante.

Las primitivas Prehash y SignPrehash resuelven este problema dividiendo la firma en dos pasos. Calculas un valor de prehash corto en el que se encuentra el mensaje, usando solo la clave pública, y se lo envías al firmante. El firmante la convierte en una firma con la clave privada, sin ver el mensaje. Con ML-DSA en el modo Mu externo (el algoritmo que admite Tink aquí), el valor previo al hash es de 69 bytes, independientemente del tamaño del mensaje.

La firma que obtienes es una firma común sobre el mensaje original: los verificadores usan la primitiva Firma digital normal y no necesitan saber que hubo dos pasos.

Antes de comenzar

Crea un conjunto de claves de ML-DSA cuyas claves tengan un requisito de ID: la variante TINK o NO_PREFIX_WITH_PREHASH_ID si no quieres un prefijo de salida en la firma resultante. Proporciona al firmante el conjunto de claves privadas y al lado de la generación previa del hash el conjunto de claves públicas correspondiente. El firmante debe tener una clave habilitada para cada ID de clave que pueda producir el lado del prehash; consulta Keysets.

Paso 1: Calcula el valor previo al hash

Ejecuta este comando dondequiera que esté el mensaje. Solo necesita el conjunto de claves públicas.

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();

Go

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
}

Paso 2: Envía el valor previo al hash al firmante

Envía el valor previo al hash a quien tenga la clave privada. No es secreto, pero debes proteger su integridad en tránsito: un atacante que pueda modificarlo en vuelo controla lo que se firma.

Paso 3: Firma el valor previo al hash

Ejecuta este comando dondequiera que esté la clave privada.

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();

Go

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
}

Paso 4: Verifica la firma

La verificación es el flujo normal de Firma digital sobre el mensaje original, no sobre el valor previo al hash.

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);

Go

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 y SignPrehash

Las primitivas Prehash y SignPrehash dividen el cálculo de una firma digital en dos pasos:

  1. Prehash solo necesita la clave pública. Convierte un mensaje de longitud arbitraria en un valor previo al hash corto y de tamaño fijo.
  2. SignPrehash necesita la clave privada. Convierte un valor previo al hash en una firma.

La firma resultante es una firma ordinaria sobre el mensaje original. La verificas con la primitiva PublicKeyVerify Firma digital normal, y los verificadores no necesitan saber (ni les importa) que la firma se produjo en dos pasos.

Trata el valor previo al hash como bytes opacos. Tiene un tamaño fijo y contiene el ID de la clave para la que se calculó, pero su diseño es parte del formato de transferencia de Tink, por lo que no debes analizar ni construir uno por tu cuenta. Si vas a portar Tink o necesitas detalles a nivel de bytes, consulta Formato de cable de Tink.

Usa este par de elementos básicos en los siguientes casos:

  • La clave de firma se encuentra en otro lugar, por ejemplo, en un HSM, en un KMS o detrás de un límite de RPC, y no deseas enviar el mensaje completo a través de ese límite.
  • El mensaje es grande y el firmante remoto aplica un límite de tamaño de solicitud.

Si no se aplica ninguna de estas opciones, usa la primitiva Firma digital simple, ya que es más sencilla y es más difícil usarla de forma incorrecta.

Conjuntos de claves

Las dos primitivas seleccionan claves de manera diferente, de la misma forma en que la firma y la verificación lo hacen para la primitiva de Firma Digital:

  • Prehash.Compute siempre usa la clave primaria del conjunto de claves públicas y registra el ID de esa clave en el valor previo al hash. Es el lado que elige con qué clave se realizará la firma.
  • SignPrehash.Sign lee el ID de clave del valor previo al hash y firma con la clave habilitada coincidente del conjunto de claves privadas. Es el lado que sigue una elección que ya hizo otra persona. Si no hay ninguna clave habilitada en el conjunto de claves que tenga ese ID, la llamada falla.

Cada clave de un conjunto de claves de SignPrehash debe tener un requisito de ID. De lo contrario, no se podrá crear el primitivo.

Garantías de seguridad mínimas

  • La firma resultante tiene las mismas propiedades que una firma producida por la primitiva Firma digital con el mismo tipo de clave.
  • Tink agrega un prefijo al valor previo al hash con 5 bytes que contienen un valor reservado especial y el ID de la clave para la que se calculó. SignPrehash firma el valor solo con esa clave. Si el valor está vinculado criptográficamente a esa clave depende del algoritmo; para External Mu ML-DSA, sí lo está. Consulta el formato de transferencia de Tink.
  • Los mensajes pueden tener una longitud arbitraria.

Aspectos que se deben tener en cuenta

  • El firmante no puede inspeccionar lo que firma. Cualquier persona que pueda llamar a SignPrehash puede obtener un mensaje arbitrario firmado, y el firmante no tiene forma de aplicar una política al contenido del mensaje. Protege el acceso a SignPrehash exactamente de la misma manera en que protegerías el acceso a PublicKeySign.
  • Protege el valor previo al hash en tránsito. No es secreto, pero un atacante que puede modificarlo en vuelo controla lo que se firma.

Elige un tipo de llave

El ML-DSA en modo External Mu, como se describe en RFC 9881, es el único algoritmo que admite Tink para Prehash y SignPrehash. Debes usar una clave ML-DSA estándar con estos elementos primitivos.

Recomendamos ML_DSA_65 para la mayoría de los casos de uso.

La clave debe tener un requisito de ID, ya que cada valor previo al hash comienza con un prefijo que contiene el ID de la clave para la que se calculó. Se aceptan las siguientes variantes:

  • TINK: La firma resultante comienza con el prefijo habitual de 5 bytes de salida de Tink.
  • NO_PREFIX_WITH_PREHASH_ID: La firma resultante no incluye ningún prefijo de salida, mientras que la clave aún tiene el ID que necesita el valor previo al hash.

No se admiten las claves que usan la variante NO_PREFIX (sin procesar) porque no tienen un ID de clave.