Skip to content

ML-DSA driver for multipart operations#770

Open
gilles-peskine-arm wants to merge 17 commits intoMbed-TLS:developmentfrom
gilles-peskine-arm:mldsa-sign-multipart-driver
Open

ML-DSA driver for multipart operations#770
gilles-peskine-arm wants to merge 17 commits intoMbed-TLS:developmentfrom
gilles-peskine-arm:mldsa-sign-multipart-driver

Conversation

@gilles-peskine-arm
Copy link
Copy Markdown
Contributor

@gilles-peskine-arm gilles-peskine-arm commented Apr 20, 2026

Add support for multipart pure ML-DSA signature at the driver layer. Resolves #635

The signature operation allocates heap memory. We will revise this later, likely when we give the signature function access to the expanded key.

Needs preceding PR: test case generation in Mbed-TLS/mbedtls-framework#300

PR checklist

  • changelog not required because: not user-facing yet
  • framework PR provided Generate multipart ML-DSA tests mbedtls-framework#300
  • TF-PSA-Crypto development PR here
  • TF-PSA-Crypto 1.1 PR not required because: new feature
  • mbedtls development PR not required because: crypto only
  • mbedtls 4.1 PR not required because: crypto only
  • mbedtls 3.6 PR not required because: crypto only
  • tests provided

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
In setup, only validation is implemented, not the storing of the key or the
start of the operation.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
@gilles-peskine-arm gilles-peskine-arm force-pushed the mldsa-sign-multipart-driver branch from ff13977 to b0aec83 Compare April 21, 2026 19:13
@gilles-peskine-arm gilles-peskine-arm added needs-work needs-review Every commit must be reviewed by at least two team members needs-reviewer This PR needs someone to pick it up for review labels Apr 22, 2026
As with the key type and algorithm macros, they are in the driver header for
now, and will move to the public API headers once MLDSA is reachable from
the API.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
In setup, only validation is implemented, not the key expansion or the
start of the operation.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
This is a deviation from the application interface, which is practical given
that the core is supposed to have a copy of the key in the key store, and
is useful because it saves the driver from needing to make its own copy.

ARM-software/psa-api#350

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Copy the expanded private key on the heap from setup to finish.
Temporarily allocate the public key on the heap during setup.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
The pattern (c1 ? v1 : c2 ? v2 : v3) in a preprocessor conditional expression
triggers a preprocessor bug in MSVC
```
fatal error C1012: unmatched parenthesis: missing ')'
```
even with Visual Studio 2019 and `/Zc:preprocessor`. Avoid this pattern.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
@gilles-peskine-arm gilles-peskine-arm added needs-preceding-pr Requires another PR to be merged first and removed needs-reviewer This PR needs someone to pick it up for review needs-ci Needs to pass CI tests labels Apr 23, 2026
Copy link
Copy Markdown
Contributor

@bjwtaylor bjwtaylor left a comment

Choose a reason for hiding this comment

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

Need to do a full review, though a quick question I had noticed.

* been set up for signing and not finished
* or aborted yet.
* \param[out] signature On success, the exported key.
* \param[in] key_buffer The key material. This must be the same seed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Key buffer and key_buffer are never used. Presumably they are specified in the API and then not needed by the function? Is there a plan to use these variables in the future and whatever the plan do we need to document the current reasoning behind this?

Copy link
Copy Markdown
Contributor Author

@gilles-peskine-arm gilles-peskine-arm Apr 24, 2026

Choose a reason for hiding this comment

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

For ML-DSA, the signature calculation needs the public key during setup, and the expanded private key during finish. The standard PSA format for the private key is just the seed. Since the same mldsa-native function calculates both the expanded private key and the public key from the seed, we store the expanded private key during sign_setup and use it in sign_finish (where a comment explains this). It's a performance/memory trade-off: we could instead recalculate the private key in sign_finish.

In addition to this memory/performance tradeoff, there are other algorithms or key representation formats where it's more advantageous to not store any key material between setup and finish. So I proposed that the driver interface would pass the key both to setup and finish. (The driver interface isn't officially defined yet.) ARM-software/psa-api#350

We plan to also support a key representation that's fully expanded, in which case there's nothing to calculate and sign_finish would read the part that it needs from key_buffer. #774

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK is it worth adding a comment here and potentially at the void points to capture this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the header, I don't think this warrants a comment: whether the parameter is used or not is an implementation detail. The API follows the applicable standard, or at least the current draft of that standard.

In the implementation, there's already a comment just above the void point.

tf_psa_crypto_mldsa_operation_t *operation,
const uint8_t *input, size_t input_length);

/** Finish a pure-ML-DSA signature operation.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: There maybe a good reason for this, however the doxygen docs list the parameters in a different order to the prototype. If there's no good reason for it, then it may be worth re-ordering them to improve readability.

const uint8_t *input, size_t input_length)
{
mld_shake256_absorb(&operation->shake, input, input_length);
return PSA_SUCCESS;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This doesn't look right, if I've understood correctly the mld_shake256_absorb calls functions that can fail. I understand they are going through the driver wrapper though, so is there no way we can pass the error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

mld_xxx functions return void.

The only cases where the functions can fail internally are:

  • If there's a bug in our code and we end up calling setup in a configuration where the algorithm is not provided. This shouldn't happen thanks to compile-time guards.
  • If SHAKE is provided by a third-party PSA driver (i.e. neither our implementation nor the one in mldsa-native) and that one has failure conditions, which is unusual, especially for an algorithm that doesn't use a secret key.

That second case is a bug, filed as #744. Given how unusual it is for it to matter, it's very low priority.

* been set up for signing and not finished
* or aborted yet.
* \param[out] signature On success, the exported key.
* \param[in] key_buffer The key material. This must be the same seed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK is it worth adding a comment here and potentially at the void points to capture this?

*signature_length = 0;

if (operation->parameter_set != 87) {
return PSA_ERROR_NOT_SUPPORTED;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, because this can't happen except if there's an internal error somewhere. I put this check in partly for robustness, and partly as a signpost to indicate that in the future, when we support multiple sizes, we'll need to have a switch on the size.

* we could instead re-expand the private key from the seed
* in \p key_buffer here. */
if (operation->key_length != MLDSA87_SECRETKEYBYTES) {
return PSA_ERROR_CORRUPTION_DETECTED;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No. Generally PSA_ERROR_CORRUPTION_DETECTED indicates that something happened that should not have happened, so there's no way to test it.

if (abort_status != PSA_SUCCESS) {
return abort_status;
}
return pqcp_to_psa_error(ret);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As above, there is no failure more for abort.

const uint8_t *signature, size_t signature_length)
{
if (operation->parameter_set != 87) {
return PSA_ERROR_NOT_SUPPORTED;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As above, it's currently impossible to get that far without having a supported size in the operation.


psa_status_t abort_status = tf_psa_crypto_mldsa_abort(operation);
if (abort_status != PSA_SUCCESS) {
return abort_status;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As above, there is no failure more for abort.

}

if (ret == MLD_ERR_FAIL) {
return PSA_ERROR_INVALID_SIGNATURE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We do.

grep -c 'verify_multipart.*PSA_ERROR_INVALID_SIGNATURE' tests/suites/test_suite_psa_crypto_mldsa.*.data

if (ret == MLD_ERR_FAIL) {
return PSA_ERROR_INVALID_SIGNATURE;
} else {
return pqcp_to_psa_error(ret);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have/need a test that covers this return?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Currently there are no possible errors from verify, other than the invalid-signature case. In the near future, there will be an INSUFFICIENT_MEMORY case.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-preceding-pr Requires another PR to be merged first needs-review Every commit must be reviewed by at least two team members priority-high High priority - will be reviewed soon size-s Estimated task size: small (~2d)

Projects

Development

Successfully merging this pull request may close these issues.

Multipart sign-message and verify-message in MLDSA driver

2 participants