-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background
FIPS 204 defines two pairs of signature generation and verification:
- ML-DSA.Sign(sk, M, ctx)
- .NET: MLDsa.SignData(data: M, context: ctx);
- HashML-DSA.Sign(sk, M, ctx, PH)
- .NET: MLDsa.SignPreHash(hash: PH(M), hashAlgorithmOid: PH.OID, context: ctx);
The two signatures are not mutually comprehensible. With the industry preferring the first ("pure") approach, but HSMs and the like dreading needing to send gigabytes, a comment in the spec made for a happy alternative:
FIPS 204, Section 6.2 (ML-DSA Signing (internal)), Algorithm 7 (ML-DSA.Sign_internal(sk, M', rnd))
...
6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) ▷ message representative that may optionally be computed in a different cryptographic module
One supposes that the intention is that libraries like Windows CNG NCrypt can compute the mu value using software, then hand it over to the HSM to finish the hardware signature. Even as HSMs are celebrating this, the primitive cryptographic libraries realized this solved the problem for them that their API design required M to be in contiguous memory... which is not pleasant if one is signing something like a 4.5GB DVD ISO.
Underlying providers are therefore exposing Sign-Mu operations to be commensurate with RSA/DSA/EC-DSA SignHash. As this is about signing a mu value that came from outside the signature operation, it is colloquially referred to as "external" mu.
API Proposal
Part 1, Sign/Verify
Both the data
and the context
values are used in the calculation of mu
, and neither appear again later in the signature algorithm. Therefore SignData(data, context)
=> SignExternalMu(mu)
.
namespace System.Security.Cryptography
{
public abstract partial class MLDsa
{
public byte[] SignExternalMu(byte[] mu);
public byte[] SignExternalMu(ReadOnlySpan<byte> mu);
public void SignExternalMu(ReadOnlySpan<byte> mu, Span<byte> destination);
protected abstract void SignExternalMuCore(ReadOnlySpan<byte> mu, Span<byte> destination);
public bool VerifyExternalMu(byte[] mu, byte[] signature);
public bool VerifyExternalMu(ReadOnlySpan<byte> mu, ReadOnlySpan<byte> signature);
protected abstract bool VerifyExternalMuCore(ReadOnlySpan<byte> mu, ReadOnlySpan<byte> signature);
}
public partial class MLDsaAlgorithm
{
public int MuSizeInBytes { get; }
}
}
Part 2, Computing Mu
- OpenSSL 3.5 exposes the hook for signing mu, but does not expose a direct way to calculate it.
- OpenSSL Main also does not expose a direct way to calculate mu, but instead is adding a way to accumulate data into a signature like how one accumulates into a hash.
- Windows does not currently have support for external mu, but based on discussions I expect them to end up like OSSL 3.5.
While mu is used for both ML-DSA.Sign and HashML-DSA.Sign, the API here is only concerned with "pure" mu, as the hash variant already has ways of accomplishing a streaming hash.
The MLDsaMuHash design is based on Shake256 and IncrementalHash.
namespace System.Security.Cryptography
{
public abstract partial class MLDsa
{
/* OpenExternalMuHash, GetMuHasher, MUUUUUUU (like a cow says), MuBabyMu, TheCowSaysMu, ... */
public MLDsaMuHash OpenExternalMuHash();
public MLDsaMuHash OpenExternalMuHash(byte[]? context);
public MLDsaMuHash OpenExternalMuHash(ReadOnlySpan<byte> context);
protected virtual MLDsaMuHash OpenExternalMuHashCore(ReadOnlySpan<byte> context);
}
[ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public abstract class MLDsaMuHash : IDisposable
{
protected MLDsaMuHash(MLDsa key);
public int HashLengthInBytes { get; }
protected MLDsa Key { get; }
public void AppendData(byte[] data);
public void AppendData(Stream stream);
public void AppendData(ReadOnlySpan<byte> data);
public Task AppendDataAsync(Stream stream, CancellationToken cancellationToken = default);
protected abstract void AppendDataCore(ReadOnlySpan<byte> data);
public MLDsaMuHash Clone();
protected abstract MLDsaMuHash CloneCore();
public void Dispose();
protected virtual void Dispose(bool disposing);
public byte[] GetCurrentHash();
public void GetCurrentHash(Span<byte> destination);
protected abstract void GetCurrentHashCore(Span<byte> destination);
public byte[] GetHashAndReset();
public void GetHashAndReset(Span<byte> destination);
protected abstract void GetHashAndResetCore(Span<byte> destination);
public void Reset();
protected virtual void ResetCore();
public byte[] SignAndReset();
public void SignAndReset(Span<byte> destination);
public bool VerifyAndReset(byte[] signature);
public bool VerifyAndReset(ReadOnlySpan<byte> signature);
}
}
Alternative Names
- MLDsaMuHash -> MLDsaMuBuilder
- Then probably rename most of the methods.
- OpenExternalMuHash -> CreateExternalMuBuilder
- Adding an instance "Create" method, and assumes a rename of Hash->Builder.
Alternative Designs
Reducing Capture
The MLDsaMuHash type captures the original MLDsa key object (whether it be public or private) to later invoke _key.SignExternalMu
or _key.VerifyExternalMu
from the Sign/Verify(AndReset) method groups.
Instead, we could remove those convenience methods and base the type on just MLDsaAlgorithm (though data from the public key is still needed at object creation time).
Matching OpenSSL
Instead of making it easy to compute the mu value, we could change the hash-builder to a signature-builder and only allow accumulating data into an ultimate call to sign or verify.
Split mu-calculation and signature accumulation into different types.
Either MLDsaSignatureBuilder : MLDsaMuBuilder
or MLDsaSignatureBuilder(MLDsaMuBuilder)
.
Either way, it seems like we then need both OpenExternalMu
and OpenSignatureBuilder
to be present on MLDsa, and use virtuals to power them.