Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ExternalMu Shuffle #65

Merged
merged 12 commits into from
Jan 14, 2025
231 changes: 145 additions & 86 deletions draft-ietf-lamps-dilithium-certificates.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ levels: ML-DSA-44, ML-DSA-65, and ML-DSA-87.

{{FIPS204}} defines two variants of ML-DSA: a pure and a prehash variant.
Only the former is specified in this document.
See {{sec-disallow-hash}} for the rationale.
The pure variant of ML-DSA supports the typical prehash flow,
see {{prehash}}. In short: one cryptographic module can compute the hash *mu*
on line 6 of algorithm 7 of {{FIPS204}} and pass it to a second module
Expand Down Expand Up @@ -401,92 +402,6 @@ in this section.
{{examples}} contains example ML-DSA private keys encoded using the
textual encoding defined in {{RFC7468}}.

# Pre-hashing (ExternalMu-ML-DSA) {#prehash}

Some applications require prehashing, where the signature generation
process can be separated into a pre-hash step and a core signature
step in order to ease operational requirements around large or
inconsistently-sized payloads. This can be performed at the
protocol layer, but not all protocols support it.
Examples in [RFC5280] are certificate and certificate revocation list
(CRL) data structures, that do not include message digesting before signing.
This can make signing large CRLs or a high volume of certificates
with large public keys challenging.

As mentioned in the introduction, pure ML-DSA signing itself
supports a prehashing flow by splitting the operation over two
modules. In this section we make this "ExternalMu-ML-DSA"
more explicit.

There are two steps. First an `ExternalMu-ML-DSA.Prehash()`
followed by `ExternalMu-ML-DSA.Sign()`. Together these are functionally
equivalent to `ML-DSA.Sign()` from [FIPS204] in that they create
exactly the same signatures as regular pure ML-DSA, which can be
verified by the unmodified `ML-DSA.Verify()`.

An ML-DSA key and certificate MAY be used with either ML-DSA
or ExternalMu-ML-DSA interchangeably.
Note that ExternalMu-ML-DSA describes a different signature API from ML-DSA
and therefore might require explicit support from hardware or
software cryptographic modules.

Note that the signing mode defined here is different from HashML-DSA
defined in [FIPS204] section 5.4. This specification uses exclusively
ExternalMu-ML-DSA for pre-hashed use cases, and thus public
keys identified by `id-hash-ml-dsa-44-with-sha512`,
`id-hash-ml-dsa-65-with-sha512`, and `id-hash-ml-dsa-87-with-sha512`
MUST NOT be used in X.509 and related PKIX protocols with the
exception of the Public Key in end-entity X.509 certifacates.
Such public keys could be used beyond PKIX.

All functions and notation used in {{fig-externalmu-ml-dsa-external}}
and {{fig-externalmu-ml-dsa-internal}} are defined in [FIPS204].

External operations:

~~~
ExternalMu-ML-DSA.Prehash(pk, M, ctx):

if |ctx| > 255 then
return error # return an error indication if the context string is
# too long
end if

M' = BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|ctx|, 1)
|| ctx) || M
mu = H(BytesToBits(H(pk, 64)) || M', 64)
return mu
~~~
{: #fig-externalmu-ml-dsa-external title="External steps of ExternalMu-ML-DSA"}



Internal operations:

~~~
ExternalMu-ML-DSA.Sign(sk, mu):

if |mu| != 512 then
return error # return an error indication if the input mu is not
# 64 bytes (512 bits).
end if

rnd = rand(32) # for the optional deterministic variant,
# set rnd to all zeroes
if rnd = NULL then
return error # return an error indication if random bit
# generation failed
end if

sigma = ExternalMu-ML-DSA.Sign_internal(sk, mu, rnd)
return sigma


ExternalMu-ML-DSA.Sign_internal(sk, mu, rnd): # mu is passed as argument instead of M'
... identical to FIPS 204 Algorithm 7, but with Line 6 removed.
~~~
{: #fig-externalmu-ml-dsa-internal title="Internal steps of ExternalMu-ML-DSA"}

# IANA Considerations

For the ASN.1 module in {{asn1}}, IANA is requested to assign an object
Expand Down Expand Up @@ -577,6 +492,52 @@ key serialization. It for this reason the public key structure
defined in {{ML-DSA-PubblicKey}} is intentionally encoded as a
single OCTET STRING.

## Rationale for disallowing HashML-DSA {#sec-disallow-hash}

The HashML-DSA mode defined in Section 5.4 of {{FIPS204}} MUST NOT be
used by CAs generating certificates or CRLs, CAs and RAs enrolling
Subcribers, OCSP responders responding; in other words, public keys
identified by `id-hash-ml-dsa-44-with-sha512`,
`id-hash-ml-dsa-65-with-sha512`, and `id-hash-ml-dsa-87-with-sha512`
MUST NOT be used in X.509 certificates and CRLs and related PKIX
protocols. The notable exception is the public key in end-entity
X.509 certificates; such public keys could be used beyond PKIX.
seanturner marked this conversation as resolved.
Show resolved Hide resolved

This restriction is for both security and implementation reasons.

The security reason for disallowing HashML-DSA is that the design of the
ML-DSA algorithm provides enhanced resistance against signature
collision attacks, compared with conventional RSA or ECDSA signature
algorithms. Specifically, ML-DSA binds the hash of the public key `tr`
to the message to-be-signed prior to hashing, as described in line 6 of
Algorithm 7 of {{FIPS204}}. In practice, this provides binding to the
indended verification public key, preventing some attacks that would
otherwise allow a signature to be successfully verified against a
non-intended public key. Also, this binding means that in the unlikely
discovery of a collision attack against SHA-3, an attacker would
have to perform a public-key-specific collision search in order to find
message pairs such that `H(tr || m1) = H(tr || m2)` since a direct hash
collision `H(m1) = H(m2)` will not suffice. HashML-DSA removes both of
these enhanced security properties and therefore is a weaker signature
algorithm.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove "therefore is a weaker signature algorithm." In practice it is not weaker because SHA-3 does not have collisions. Better to not be too alarmist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a less alarmist wording that still captures the same idea?

Maybe "... and therefore weakens some of the security properties built in to the ML-DSA design" ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am proposing the below rephrase

[...] Also, this binding means that in the unlikely, theoretical case of a collision attack against SHA-3, an attacker would have to perform a public-key-specific collision search in order to find message pairs such that H(tr || m1) = H(tr || m2) since a simple hash collision H(m1) = H(m2) will not suffice. HashML-DSA removes both of these enhanced security properties. and therefore is a weaker signature algorithm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the length and order of paragraphs gives undue emphasis to the security aspect. I don't mind mentioning the difference, but it should be toned down.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concretely, can we move the security paragraph second?

seanturner marked this conversation as resolved.
Show resolved Hide resolved

The implementation reason for disallowing HashML-DSA stems from the fact
that ML-DSA and HashML-DSA are incompatible algorithms that require
different `Verify()` routines. This forwards to the protocol the
complexity of informing the client whether to use `ML-DSA.Verify()` or
`HashML-DSA.Verify()`. Additionally, since
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there is a hash that has to be specified in HashML-DSA.

seanturner marked this conversation as resolved.
Show resolved Hide resolved
the same OIDs are used to identify the ML-DSA
public keys and ML-DSA signature algorithms, an implementation would
need to commit a given public key to be either of type `ML-DSA` or
`HashML-DSA` at the time of certificate creation. This is anticipated
to cause operational issues in contexts where the operator does not
know at key generation time whether the key will need to produce pure
or pre-hashed signatures. ExternalMu-ML-DSA avoids all of these
operational concerns by virtue of having keys and signatures that are
indistinguishable from ML-DSA (i.e., ML-DSA and ExternalMu-ML-DSA are
mathematically equivalent algorithms). The difference between ML-DSA
and ExternalMu-ML-DSA is merely an internal implementation detail of
the signer and has no impact on the verifier or network protocol.

--- back

Expand Down Expand Up @@ -747,6 +708,104 @@ previous section.
{::include ./examples/ML-DSA-44.crt.txt}
~~~

# Pre-hashing (ExternalMu-ML-DSA) {#prehash}

Some applications require pre-hashing, where the signature generation
process can be separated into a pre-hash step and a core signature
step in order to ease operational requirements around large or
inconsistently-sized payloads. Pre-hashing can be performed at the
protocol layer, but not all protocols support it. Examples in
{{RFC5280}} are certificates and CRLs; these do not include message
digesting before signing. This can make signing large CRLs or a high
volume of certificates with large public keys challenging.

As mentioned in the introduction, pure ML-DSA signing itself
supports a pre-hashing flow by splitting the operation over two
modules. In this section, we make this "ExternalMu-ML-DSA"
more explicit.

There are two steps. First an `ExternalMu-ML-DSA.Prehash()`
followed by `ExternalMu-ML-DSA.Sign()`. Together these are functionally
equivalent to `ML-DSA.Sign()` from {{FIPS204}} in that used in sequence
they create exactly the same signatures as regular pure ML-DSA, which
can be verified by the unmodified `ML-DSA.Verify()`.

An ML-DSA key and certificate can be used with either ML-DSA
or ExternalMu-ML-DSA interchangeably.
Note that ExternalMu-ML-DSA describes a different signature API from ML-DSA
and therefore might require explicit support from hardware or
software cryptographic modules.

Note that the signing mode defined here is different from HashML-DSA
defined in Section 5.4 of {{FIPS204}}. This specification uses exclusively
ExternalMu-ML-DSA for pre-hashed use cases. See {{sec-disallow-hash}} for
additional discussion of why HashML-DSA is disallowed in PKIX.

All functions and notation used in {{fig-externalmu-ml-dsa-external}}
and {{fig-externalmu-ml-dsa-internal}} are defined in {{FIPS204}}.

External operations:

~~~
ExternalMu-ML-DSA.Prehash(pk, M, ctx):

if |ctx| > 255 then
return error # return an error indication if the context string is
# too long
end if

M' = BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|ctx|, 1)
|| ctx) || M
mu = H(BytesToBits(H(pk, 64)) || M', 64)
return mu
~~~
{: #fig-externalmu-ml-dsa-external title="External steps of ExternalMu-ML-DSA"}

Internal operations:

~~~
ExternalMu-ML-DSA.Sign(sk, mu):

if |mu| != 512 then
return error # return an error indication if the input mu is not
# 64 bytes (512 bits).
end if

rnd = rand(32) # for the optional deterministic variant,
# set rnd to all zeroes
if rnd = NULL then
return error # return an error indication if random bit
# generation failed
end if

sigma = ExternalMu-ML-DSA.Sign_internal(sk, mu, rnd)
return sigma

ExternalMu-ML-DSA.Sign_internal(sk, mu, rnd): # mu is passed as argument instead of M'
... identical to FIPS 204 Algorithm 7, but with Line 6 removed.
~~~
{: #fig-externalmu-ml-dsa-internal title="Internal steps of ExternalMu-ML-DSA"}

ExternalMu-ML-DSA requires the public key, or its prehash, as input to
the pre-digesting function. This assumes the signer generating the
pre-hash is in possession of the public key before signing and is
different from conventional pre-hashing which only requires the
message and the hash function as input.

Security-wise, during the signing operation of pure (or "one-step")
ML-DSA, the cryptographic module extracts the public key hash `tr` from
the secret key object, and thus there is no possibility of mismatch
between `tr` and `sk`. In ExternalMu-ML-DSA, the public key or its hash
needs to be provided to the `Prehash()` routine indpedendly of the secret
key, and while the exact mechanism by which it is delivered will be
implementation-specific, it does open a windown for mismatches between
`tr` and `sk`. First, this will produce a signature which will fail to
verify under the intended public key since a compliant `Verify()` routine
will independently compute `tr` from the public key. Implementors should pay careful
attention to how the public key or its hash is delivered to the
`ExternalMu-ML-DSA.Prehash()` routine, and from where they are sourcing
this data.

# Acknowledgments
{:numbered="false"}

Expand Down
Loading