Skip to content

Conversation

@mkannwischer
Copy link
Contributor

Previously, the signature would be declassified in the end of signing (which is the most common threat model).
However, for rare use cases the signature may be secret in which case it is useful to know which part of the code has to be adapted.

This commit removes the declassification from the end of signing, explicitly marks the signature as secret in the constant time tests (to also classify the previously declassified challenge c), and then adds necessary declassifications in verifiation.

We may consider eliminating some of those declassifications later. In particular, the data dependencies in use_hint are not hard to eliminate (in fact, the native implementations are already constant time). The final comparison of the challenges can also easily be turned into a constant-time comparison.

@mkannwischer mkannwischer force-pushed the verify-cleaner-declassification branch from 19032f8 to b863e4d Compare December 31, 2025 01:45
@mkannwischer mkannwischer marked this pull request as ready for review December 31, 2025 02:50
@mkannwischer mkannwischer requested a review from a team as a code owner December 31, 2025 02:50
mldsa/src/sign.c Outdated
Comment on lines 910 to 916
/* Constant-time: The signature is commonly considered public. However, we
* mark it as secret to make explicit which parts of the code take time
* depending on the signature. mld_unpack_sig uses conditional branches to
* unpack the hint vector h. */
MLD_CT_TESTING_DECLASSIFY(
sig + MLDSA_CTILDEBYTES + MLDSA_L * MLDSA_POLYZ_PACKEDBYTES,
MLDSA_POLYVECH_PACKEDBYTES);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure a public API should change the classification over user-provided data -- this can break or unexpectedly alter CT tests in integrations. Can we instead document that verify is not CT for the (relevant parts of) the signature, and leave all classification to the tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The downside of your proposal is that it does not make it apparent which operations leak.
With the current approach, it is very clear which function leaks what (and it is checked in CI, so you cannot introduce additional leaks).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But, yes, we can move the declassification of the h-component to the tests insetead.
I do want to keep testing with the remainder of the signature marked as secret though.

Copy link
Contributor

@hanno-becker hanno-becker left a comment

Choose a reason for hiding this comment

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

Toplevel Q first before checking the details: I don't think the public API should change the classification of user-provided buffers, or am I overlooking something?

Previously, the signature would be declassified in the end of
signing (which is the most common threat model).
However, for rare use cases the signature may be secret in which
case it is useful to know which part of the code has to be
adapted.

This commit removes the declassification from the end of signing,
explicitly marks the signature as secret in the constant time tests
(to also classify the previously declassified challenge c), and
then adds necessary declassifications in verifiation.

We may consider eliminating some of those declassifications later.
In particular, the data dependencies in use_hint are not hard to
eliminate (in fact, the native implementations are already constant
time). The final comparison of the challenges can also easily
be turned into a constant-time comparison.

Signed-off-by: Matthias J. Kannwischer <matthias@kannwischer.eu>
@mkannwischer mkannwischer force-pushed the verify-cleaner-declassification branch from b863e4d to f72d82f Compare December 31, 2025 08:22
@mkannwischer
Copy link
Contributor Author

Toplevel Q first before checking the details: I don't think the public API should change the classification of user-provided buffers, or am I overlooking something?

I have pushed an alternative that moves the declassification of h to the test harness. Not sure if this is necessarily easier to follow.

Comment on lines +909 to +911
/* Constant-time: The signature is commonly considered public. However, we
* mark it as secret to make explicit which parts of the code take time
* depending on the signature.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment seems outdated?

Comment on lines +913 to +914
* mld_unpack_sig uses conditional branches to unpack the hint vector h.
* The h-component of the signature, hence, needs to be declassified.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* mld_unpack_sig uses conditional branches to unpack the hint vector h.
* The h-component of the signature, hence, needs to be declassified.
* mld_unpack_sig uses conditional branches to unpack the hint vector h.
* It is the caller's responsibility to declassify the h-component of the signature.

Comment on lines +949 to +950
/* Constant-time: poly_challenge uses conditional branches depending on c. */
MLD_CT_TESTING_DECLASSIFY(c, MLDSA_CTILDEBYTES);
Copy link
Contributor

Choose a reason for hiding this comment

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

In the current version, the implementation is inconsistent regarding what is declassified by the function vs. expected to be declassified by the call-site: The h-component is expected to be declassified by the call-site, but the c-component is declassified here.

Copy link
Contributor

@hanno-becker hanno-becker left a comment

Choose a reason for hiding this comment

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

We're still lacking a consistent story for how (de)classification is handled.

We rely on the caller of sign to classify the signature. When we verify, we expect the caller to again have declassified some parts of it, while others are declassified ad-hoc.

Zooming out a little, I think we should strive for the following:

  • For every const input to a top-level API, the classification status of the bytes is not changed by the implementation. This is natural if you view const as extending to the (value, secret_status) presentation of data in the CT model. It also helps to avoid surprises when integrating with CT tests of consuming libraries.
  • For every output to a top-level API, the API should say explicitly what its classification status will be.

At the moment, for example, keygen outputs a private key buffer parts of which are classified, and parts of which are declassified. This is not documented at the API-level, and it's not even obvious from the implementation. We should either say which parts are (de)classified and enforce it at the end of signing, or simply say that all of the private key buffer is secret, enforce it in the implementation, and leave it to the caller to alter that if they wish to.

For signing, it's similar: We should say explicitly whether the signature is classified as secret or public, enforce it in the implementation, and adjust the call-sites (for us, the tests) accordingly.

Note that extending const to the security-state does not imply that the implementation cannot declassify derived data from secret const inputs. Here, I'm not yet sure what to do. We can either a) declassify the relevant derived data, or b) where possible, expect the caller to have declassified the input data accordingly, and document that assumption. The latter is not always an option: For example, signing consumes the secret sk buffer, but safely declassifies derived data encountered during signature generation. For, for verification, we could document that the relevant parts of the signature must already have been declassified, and not do any ad-hoc declassification in the implementation.

mkannwischer added a commit that referenced this pull request Jan 3, 2026
Alternative to #822 that
I hope to be less controversial.
Currently the constant time tests for verification rely on the signature
being declassified at the end of verification. This is not ideal.
This commit moves this declassification to the constant-time test instead.

As suggested in
#822 (review),
there is more work left to clean up the story around declassifications.
This PR is a first step towards cleaning up that story to unblock
#825 and
#821, but there is more
work left.

Signed-off-by: Matthias J. Kannwischer <matthias@kannwischer.eu>
@mkannwischer mkannwischer marked this pull request as draft January 3, 2026 00:52
@mkannwischer
Copy link
Contributor Author

Thanks @hanno-becker for the review.
I decided to first implement a minimal change in #835 eliminating the declassification in signing that was bothering me.
Let's revisit the general declassification story separately.

mkannwischer added a commit that referenced this pull request Jan 3, 2026
Alternative to #822 that
I hope to be less controversial.
Currently the constant time tests for verification rely on the signature
being declassified at the end of verification. This is not ideal.
This commit moves this declassification to the constant-time test instead.

As suggested in
#822 (review),
there is more work left to clean up the story around declassifications.
This PR is a first step towards cleaning up that story to unblock
#825 and
#821, but there is more
work left.

Signed-off-by: Matthias J. Kannwischer <matthias@kannwischer.eu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants