Quiero firmar datos con un firmante remoto

Recomendamos las primitivas Prehash y SignPrehash con una clave ML_DSA_44 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, solo con la clave pública, y lo envías al firmante. El firmante la convierte en una firma con la clave privada, sin ver nunca 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 deseas un prefijo de salida en la firma resultante. Proporciona al firmante el conjunto de claves privadas y al lado previo al 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 esto 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();

Comienza a usarlo

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
}

Python

from tink import signature

# Due to the internal structure of Tink Python, Prehash and SignPrehash
# are an exception where calling signature.register() is not necessary.
# Do not expect the same for other primitives (such as PublicKeyVerify in
# Step 4).
prehasher = public_handle.primitive(signature.Prehash)
prehash = prehasher.compute(message)

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

Envía el valor previo al hash a quienquiera que tenga la clave privada. No es secreto, pero debes proteger su integridad en tránsito: un atacante que pueda modificarlo en tránsito 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();

Comienza a usarlo

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
}

Python

from tink import signature

# Due to the internal structure of Tink Python, Prehash and SignPrehash
# are an exception where calling signature.register() is not necessary.
# Do not expect the same for other primitives (such as PublicKeyVerify in
# Step 4).
signer = private_handle.primitive(signature.SignPrehash)
sig = signer.sign(prehash)

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

Comienza a usarlo

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
}

Python

from tink import signature

signature.register()

verifier = public_handle.primitive(signature.PublicKeyVerify)
verifier.verify(sig, message)

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 de prehash 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 analizarlo ni construirlo 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 primitivos 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 quieres 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 la 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 manera que la firma y la verificación lo hacen para la primitiva de Firma Digital:

  • Prehash.Compute siempre usa la clave principal 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 la 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 elemento 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 antepone al valor previo al hash 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 ML-DSA externo de Mu, 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 durante la transmisión. No es secreto, pero un atacante que puede modificarlo en vuelo controla lo que se firma.

Elige un tipo de clave

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

Recomendamos ML_DSA_44 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.