For RSA, we could either have the function return an error code like
NOT_IMPLEMENTED or just run while disregarding ecp_max_ops. IMO the second
option makes more sense, as otherwise the caller would need to check whether
the key is EC or RSA before deciding to call either sign() or
sign_restartable(), and having to do this kind of check feels contrary to the
goal of the PK layer.
Two different changes:
- the first one will allow us to store k in the restart context while
restarting the following ecp_mul() operation
- the second one is an simplification, unrelated to restartability, made
possible by the fact that ecp_gen_privkey() is now public
(Unrelated to restartable work, just noticed while staring at the code.)
Checking at the end is inefficient as we might give up when we just generated
a valid signature or key.
Test relies on deterministic signature as this uses plain sig internally, so
if deterministic works, then so does non-deterministic, while the reciprocal
is false. (Also, deterministic is enabled by default in config.h.)
Test case is taken from a RFC 6979 test vector, just manually converting (r,s)
to the encoded signature.
Otherwise code that uses these functions in other modules will have to do:
#if defined(MBEDTLS_ECP_RESTARTABLE)
ret = do_stuff( there, may, be, many, args );
#else
ret = do_stuff( their, may, be, namy, args, rs_ctx );
#fi
and there is a risk that the arg list will differ when code is updated, and
this might not be caught immediately by tests because this depends on a
config.h compile-time option which are harder to test.
Always declaring the restartable variants of the API functions avoids this
problem; the cost in ROM size should be negligible.
This will be useful for restartable ECDH and ECDSA. Currently they call
mbedtls_ecp_gen_keypair(); one could make that one restartable, but that means
adding its own sub-context, while ECDH and ECDSA (will) have their own
contexts already, so switching to this saves one extra context.
This should only be done in the top-level function.
Also, we need to know if we indeed are the top-level function or not: for
example, when mbedtls_ecp_muladd() calls mbedtls_ecp_mul(), the later should
not reset ops_done. This is handled by the "depth" parameter in the restart
context.
When a restartable function calls another restartable function, the current
ops_count needs to be shared to avoid either doing too many operations or
returning IN_PROGRESS uselessly. So it needs to be in the top-level context
rather than a specific sub-context.
This was intended to detect aborted operations, but now that case is handled
by the caller freeing the restart context.
Also, as the internal sub-context is managed by the callee, no need for the
caller to free/reset the restart context between successful calls.
EC-JPAKE warning is no longer needed as we now have separate _restartable()
functions, and JPAKE will just call the non-restartable version.
Concurrency warning removed as this is one of the reasons why this design was
chosen.
Following discussion in the team, it was deemed preferable for the restart
context to be explicitly managed by the caller.
This commits in the first in a series moving in that directly: it starts by
only changing the public API, while still internally using the old design.
Future commits in that series will change to the new design internally.
The test function was simplified as it no longer makes sense to test for some
memory management errors since that responsibility shifted to the caller.
It's going to be convenient for each function that can generate a
MBEDTLS_ERR_ECP_IN_PROGRESS on its own (as opposed to just passing it around)
to have its own restart context that they can allocate and free as needed
independently of the restart context of other functions.
For example ecp_muladd() is going to have its own restart_muladd context that
in can managed, then when it calls ecp_mul() this will manage a restart_mul
context without interfering with the caller's context.
So, things need to be renames to avoid future name clashes.
From a user's perspective, you want a "basic operation" to take approximately
the same amount of time regardless of the curve size, especially since max_ops
is a global setting: otherwise if you pick a limit suitable for P-384 then
when you do an operation on P-256 it will return way more often than needed.
Said otherwise, a user is actually interested in actual running time, and we
do the API in terms of "basic ops" for practical reasons (no timers) but then
we should make sure it's a good proxy for running time.
Ok, so the original plan was to make mpi_inv_mod() the smallest block that
could not be divided. Updated plan is that the smallest block will be either:
- ecp_normalize_jac_many() (one mpi_inv_mod() + a number or mpi_mul_mpi()s)
- or the second loop in ecp_precompute_comb()
With default settings, the minimum non-restartable sequence is:
- for P-256: 222M
- for P-384: 341M
This is within a 2-3x factor of originally planned value of 120M. However,
that value can be approached, at the cost of some performance, by setting
ECP_WINDOW_SIZE (w below) lower than the default of 6. For example:
- w=4 -> 166M for any curve (perf. impact < 10%)
- w=2 -> 130M for any curve (perf. impact ~ 30%)
My opinion is that the current state with w=4 is a good compromise, and the
code complexity need to attain 120M is not warranted by the 1.4 factor between
that and the current minimum with w=4 (which is close to optimal perf).
This is the easy part: with the current steps, all information between steps
is passed via T which is already saved. Next we'll need to split at least the
first loop, and maybe calls to normalize_jac_many() and/or the second loop.
Separating main computation from filling of the auxiliary array makes things
clearer and easier to restart as we don't have to remember the in-progress
auxiliary array.
Previously there were only two states:
- T unallocated
- T allocated and valid
Now there are three:
- T unallocated
- T allocated and in progress
- T allocated and valid
Introduce new bool T_ok to distinguish the last two states.
Free it as soon as it's no longer needed, but as a backup free it in
ecp_group_free(), in case ecp_mul() is not called again after returning
ECP_IN_PROGRESS.
So far we only remember it when it's fully computed, next step is to be able
to compute it in multiple steps.