Quero assinar dados com um assinante remoto

Recomendamos as primitivas Prehash e SignPrehash com uma chave ML_DSA_65 quando a chave privada está em um lugar onde a mensagem não pode ser enviada.

Às vezes, a parte que detém a chave de assinatura não pode ou não deve receber a mensagem em si: a chave fica em um HSM ou KMS, ou a mensagem é maior do que o limite de tamanho da solicitação do signatário.

As primitivas Prehash e SignPrehash resolvem isso dividindo a assinatura em duas etapas. Você calcula um valor de pré-hash curto onde a mensagem está, usando apenas a chave pública, e envia para o signatário. O assinante a transforma em uma assinatura usando a chave privada, sem nunca ver a mensagem. Com o ML-DSA no modo External Mu, que o algoritmo Tink aceita, o valor de pré-hash é de 69 bytes, seja qual for o tamanho da mensagem.

A assinatura que você recebe é uma assinatura comum da mensagem original: os verificadores usam a primitiva regular Assinatura digital e não precisam ter necessidade de saber que duas etapas foram envolvidas.

Antes de começar

Crie um conjunto de chaves ML-DSA cujas chaves tenham um requisito de ID: a variante TINK ou NO_PREFIX_WITH_PREHASH_ID se você não quiser um prefixo de saída na assinatura resultante. Dê ao signatário o conjunto de chaves privadas e ao lado de pré-hash o conjunto de chaves públicas correspondente. O signatário precisa ter uma chave ativada para cada ID de chave que o lado de pré-hash pode produzir. Consulte Keysets.

Etapa 1: calcular o valor de pré-hash

Execute isso em qualquer lugar em que a mensagem esteja. Ele só precisa do conjunto de chaves 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
}

Etapa 2: envie o valor de pré-hash ao assinante

Envie o valor de pré-hash para o que tiver a chave privada. Ele não é secreto, mas você precisa proteger a integridade dele em trânsito: um invasor que pode modificar o token em voo controla o que é assinado.

Etapa 3: assinar o valor de pré-hash

Execute isso onde quer que a chave privada esteja.

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
}

Etapa 4: verificar a assinatura

A verificação é o fluxo comum de assinatura digital na mensagem original, não no valor de pré-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 e SignPrehash

As primitivas Prehash e SignPrehash dividem o cálculo de uma assinatura digital em duas etapas:

  1. O prehash só precisa da chave pública. Ele transforma uma mensagem de comprimento arbitrário em um valor de pré-hash curto e de tamanho fixo.
  2. O SignPrehash precisa da chave privada. Ele transforma um valor de pré-hash em uma assinatura.

A assinatura resultante é uma assinatura comum da mensagem original. Você o verifica com a primitiva Assinatura digital PublicKeyVerify regular, e os verificadores não precisam saber nem se importar que a assinatura foi produzida em duas etapas.

Trata o valor de pré-hash como bytes opacos. Ele tem um tamanho fixo e carrega o ID da chave para a qual foi calculado, mas o layout faz parte do formato de fio do Tink, e você não deve analisar nem construir um por conta própria. Se você estiver migrando o Tink ou precisar dos detalhes no nível de byte, consulte Formato de fio do Tink.

Use esse par de primitivas quando:

  • A chave de assinatura está em outro lugar, por exemplo, em um HSM, em um KMS ou atrás de um limite de RPC, e você não quer enviar a mensagem completa por esse limite.
  • A mensagem é grande, e o assinante remoto impõe um limite de tamanho para a solicitação.

Se nenhuma delas se aplicar, use a primitiva Assinatura digital simples. Ela é mais fácil de usar e mais difícil de usar de forma indevida.

Conjunto de chaves

As duas primitivas selecionam chaves de maneira diferente, da mesma forma que a assinatura e a verificação fazem para a primitiva Assinatura digital:

  • Prehash.Compute sempre usa a chave primária do conjunto de chaves públicas e registra o ID dessa chave no valor de pré-hash. É o lado que escolhe com qual chave a assinatura será feita.
  • SignPrehash.Sign lê o ID da chave do valor de pré-hash e assina com a chave ativada correspondente do conjunto de chaves privadas. É o lado que segue uma escolha já feita por outra pessoa. Se nenhuma chave ativada no conjunto de chaves tiver esse ID, a chamada vai falhar.

Cada chave em um conjunto de chaves SignPrehash precisa ter um requisito de ID. Caso contrário, a criação da primitiva falha.

Garantias mínimas de segurança

  • A assinatura resultante tem as mesmas propriedades de uma assinatura produzida pela primitiva Assinatura digital com o mesmo tipo de chave.
  • O Tink adiciona ao valor pré-hash um prefixo de 5 bytes que contém um valor especial reservado e o ID da chave para a qual ele foi calculado. SignPrehash assina o valor apenas com essa chave. Se o valor está criptograficamente vinculado a essa chave depende do algoritmo. Para Mu ML-DSA externa, sim. Consulte Formato de fio do Tink.
  • As mensagens podem ter qualquer tamanho.

Itens a serem observados

  • O assinante não pode inspecionar o que assina. Qualquer pessoa que possa ligar para SignPrehash pode receber uma mensagem arbitrária assinada, e o signatário não tem como aplicar uma política ao conteúdo da mensagem. Proteja o acesso a SignPrehash da mesma forma que você protege o acesso a PublicKeySign.
  • Proteja o valor de pré-hash em trânsito. Ele não é secreto, mas um invasor que pode modificá-lo em voo controla o que é assinado.

Escolher um tipo de chave

O ML-DSA no modo External Mu, conforme descrito na RFC 9881, é o único algoritmo compatível com a Tink para Prehash e SignPrehash. Use uma chave ML-DSA padrão com essas primitivas.

Recomendamos ML_DSA_65 para a maioria dos casos de uso.

A chave precisa ter um requisito de ID, porque todo valor de pré-hash começa com um prefixo que contém o ID da chave para a qual ele foi calculado. As seguintes variantes são aceitas:

  • TINK: a assinatura resultante começa com o prefixo de saída de 5 bytes usual do Tink.
  • NO_PREFIX_WITH_PREHASH_ID: a assinatura resultante não tem um prefixo de saída, mas a chave ainda tem o ID de que o valor de pré-hash precisa.

As chaves que usam a variante NO_PREFIX (bruta) não são compatíveis porque não têm um ID.