The Prehash and SignPrehash primitives split the computation of a digital signature into two steps:
- Prehash needs only the public key. It turns a message of arbitrary length into a short, fixed-size prehash value.
- SignPrehash needs the private key. It turns a prehash value into a signature.
The signature that comes out is an ordinary signature over the original
message. You verify it with the regular
Digital Signature PublicKeyVerify primitive, and
verifiers don't need to know -- or care -- that the signature was produced in
two steps.
Treat the prehash value as opaque bytes. It has a fixed size and it carries the ID of the key it was computed for, but its layout is part of Tink's wire format, and you should not parse or construct one yourself. If you are porting Tink or need the byte-level details, see Tink wire format.
Use this pair of primitives when:
- The signing key lives somewhere else, for example in an HSM, in a KMS, or behind an RPC boundary, and you don't want to ship the full message across that boundary.
- The message is large, and the remote signer enforces a request size limit.
If neither of these applies, use the plain Digital Signature primitive instead: it is simpler, and it is harder to misuse.
Keysets
The two primitives select keys differently, in the same way that signing and verification do for the Digital Signature primitive:
Prehash.Computealways uses the primary key of the public keyset, and records that key's ID in the prehash value. It is the side that chooses which key the signature will be made with.SignPrehash.Signreads the key ID out of the prehash value and signs with the matching enabled key of the private keyset. It is the side that follows a choice someone else has already made. If no enabled key in the keyset has that ID, the call fails.
Every key in a SignPrehash keyset must have an ID requirement, otherwise creating the primitive fails.
Minimal security guarantees
- The resulting signature has the same properties as a signature produced by the Digital Signature primitive with the same key type.
- Tink prefixes the prehash value with 5 bytes containing a special reserved
value and the ID of the key it was computed for.
SignPrehashsigns the value only with that key. Whether the value is cryptographically bound to that key depends on the algorithm; for External Mu ML-DSA it is, see Tink wire format. - Messages can have arbitrary length.
Things to watch out for
- The signer cannot inspect what it signs. Anyone who can call
SignPrehashcan get an arbitrary message signed, and the signer has no way to apply a policy to the message contents. Protect access toSignPrehashexactly as you would protect access toPublicKeySign. - Protect the prehash value in transit. It is not secret, but an attacker who can modify it in flight controls what gets signed.
Choose a key type
ML-DSA in External Mu mode, as described in RFC 9881, is the only algorithm Tink supports for Prehash and SignPrehash. You should use a standard ML-DSA key with these primitives.
We recommend ML_DSA_65 for most use cases.
The key must have an ID requirement, because every prehash value starts with a prefix that contains the ID of the key it was computed for. The following variants are accepted:
TINK-- the resulting signature starts with Tink's usual 5-byte output prefix.NO_PREFIX_WITH_PREHASH_ID-- the resulting signature carries no output prefix, while the key still has the ID that the prehash value needs.
Keys that use the NO_PREFIX (raw) variant are not supported, because they
have no key ID.
Examples
The following examples sign a message in two steps and then verify the resulting signature with the ordinary Digital Signature primitive.
They are shown as one block for readability. In a real deployment the two steps run in different places; see I want to sign data with a remote signer for a step-by-step walkthrough.
C++
#include "tink/keyset_handle.h" #include "tink/public_key_verify.h" #include "tink/signature/config_2026.h" #include "tink/signature/prehash.h" #include "tink/signature/sign_prehash.h" using ::crypto::tink::ConfigSignature2026; using ::crypto::tink::Prehash; using ::crypto::tink::PublicKeyVerify; using ::crypto::tink::SignPrehash; // 1. Wherever the message is: compute the prehash value. This needs only // the public keyset. absl::StatusOr<std::unique_ptr<Prehash>> prehasher = public_handle.GetPrimitive<Prehash>(ConfigSignature2026()); if (!prehasher.ok()) return prehasher.status(); absl::StatusOr<std::string> prehash = (*prehasher)->Compute(message); if (!prehash.ok()) return prehash.status(); // 2. Wherever the private key is: turn the prehash value into a signature. absl::StatusOr<std::unique_ptr<SignPrehash>> signer = private_handle.GetPrimitive<SignPrehash>(ConfigSignature2026()); if (!signer.ok()) return signer.status(); absl::StatusOr<std::string> signature = (*signer)->Sign(*prehash); if (!signature.ok()) return signature.status(); // 3. Anywhere: verify the signature over the original message. absl::StatusOr<std::unique_ptr<PublicKeyVerify>> verifier = public_handle.GetPrimitive<PublicKeyVerify>(ConfigSignature2026()); if (!verifier.ok()) return verifier.status(); absl::Status verified = (*verifier)->Verify(*signature, message);
Go
import ( "github.com/tink-crypto/tink-go/v2/signature" "github.com/tink-crypto/tink-go/v2/signprehash" ) // 1. Wherever the message is: compute the prehash value. This needs only // the public keyset. prehasher, err := signprehash.NewPrehash(publicHandle) if err != nil { return err } prehash, err := prehasher.ComputePrehash(message) if err != nil { return err } // 2. Wherever the private key is: turn the prehash value into a signature. signer, err := signprehash.NewPrehashSigner(privateHandle) if err != nil { return err } sig, err := signer.SignPrehash(prehash) if err != nil { return err } // 3. Anywhere: verify the signature over the original message. verifier, err := signature.NewVerifier(publicHandle) if err != nil { return err } if err := verifier.Verify(sig, message); err != nil { return err }
Related topics
- I want to sign data with a remote signer -- a step-by-step walkthrough of the two-step flow.
- Digital signature -- sign and verify in a single step, when the signer can see the message.
- Supported primitive key types by language
- Tink wire format -- the byte-level layout of a prehash value, for porting Tink to another language.