Skip to content

Commit 4acce0a

Browse files
committed
sign: Add configurable signing hooks (resume/attempt/finish)
The runtime of ML-DSA signatures is theoretically unbounded. Even the bound of max 814 signature attempts -- approved by NIST as lowering the chance of signing failure below 2^{-256} -- leaves a performance distribution whose peak percentiles can be unacceptable for constrained systems with strict timing requirements. As a remedy, the library already provides a configuration option MLD_CONFIG_MAX_SIGNING_ATTEMPTS to limit the number of signing attempts. However, FIPS 204 explicitly forbids lowering the number of signing attempts below 814, rendering the use of this option in constrained environments non-compliant. As a remedy, this commit introduces a facility for _restartable signing_: In addition to using MLD_CONFIG_MAX_SIGNING_ATTEMPTS, the user can provide a custom hook for remembering the last signing attempt, and continuing the next signing call at that point. More specifically, the idea is to introduce three 'signing hooks' into the core signature routine: - `nonce = sign_resume()`: Return the nonce corresponding to the next signing attempt. `0` for an initial / non-restartable signing operation. - `sign_attempt(nonce)`: Note the nonce for the next signing attempt. - `sign_finish(nonce)`: Note the nonce for which signing succeeded. Here, only `sign_resume()` affects the signing process functionally, while `sign_attempt()` and `sign_finish()` are passive logging operations. This approach to restartable signing only works if the consumer works directly with the internal signing API. If they use the randomized API, a fresh seed would be chosen even on supposedly restarted signing operations, leading to non-compliant signatures. The use of a `sign_resume()` hook is therefore only safe with MLD_CONFIG_NO_RANDOMIZED_API. In contrast, `sign_attempt` and `sign_finish` are passive, and can safely be used on their own with any configuration. Indeed, these hooks serve other uses outside of restartable signing: - sign_attempt can be used to get finer-grained benchmarks for individual signing attempts - sign_finish can be used to log the number of signing attempts, which can be useful again for benchmarking, or for checking the expected distribution of signing attempts. Finally, sign_{resume|attempt|finish} can all be implemented based on mutable globals, or based on a context. It therefore makes sense to introduce them under the existing MLD_CONFIG_CONTEXT_PARAMETER wrapper, which allows functions to be called with or without an application-specific context pointer. Taking all these considerations together, here's what the commit changes: - Add independent MLD_CONFIG_SIGN_HOOK_{RESUME|FINISH|ATTEMPT} configuration options for registering signing hooks as above. When set, the user provides the following functions, respectively: - `nonce = mld_sign_resume([ctx])`: Provide initial or resuming nonce - `mld_sign_attempt(nonce [, ctx])`: Log/remember nonce for next attempt - `mld_sign_finish(nonce [, ctx])`: Log/remember nonce for successful attempt - The option can be used with or without MLD_CONFIG_CONTEXT_PARAMETER. If the latter is set, the user-provided context will be passed to `mld_sign_xxx` as the last argument. - An example is added illustrating the use of the hooks to implement restartable signing. Note: A consumer might want to use restartable signing with randomized APIs. This is not natively supported. However, it is expected to be easily implementable since the components required to build randomized signing from deterministic signing are all public. Note that resuming additionally requires the consumer to hold the signing randomness `rnd` fixed across the resumed calls. Signed-off-by: Hanno Becker <beckphan@amazon.co.uk>
1 parent a7f9094 commit 4acce0a

59 files changed

Lines changed: 4822 additions & 239 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/config-variations/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ runs:
3535
acvp: true
3636
opt: ${{ inputs.opt }}
3737
examples: true
38-
extra_args: "--exclude-example basic_deterministic"
38+
extra_args: "--exclude-example basic_deterministic --exclude-example restartable_sign"
3939
- name: "REDUCE_RAM"
4040
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'reduce-ram') }}
4141
uses: ./.github/actions/multi-functest
@@ -64,7 +64,7 @@ runs:
6464
alloc: true
6565
opt: ${{ inputs.opt }}
6666
examples: true
67-
extra_args: "--exclude-example basic_deterministic"
67+
extra_args: "--exclude-example basic_deterministic --exclude-example restartable_sign"
6868
- name: "PCT enabled + broken"
6969
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'pct-enabled-broken') }}
7070
shell: bash

.github/workflows/base.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ jobs:
233233
- name: basic_lowram
234234
run: |
235235
CFLAGS="-O0" make run -C examples/basic_lowram
236+
- name: restartable_sign
237+
run: |
238+
CFLAGS="-O0" make run -C examples/restartable_sign
236239
- name: bring_your_own_fips202
237240
run: |
238241
CFLAGS="-O0" make run -C examples/bring_your_own_fips202

BIBLIOGRAPHY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ source code and documentation.
8282
- [examples/monolithic_build_native/mldsa_native/mldsa_native_config.h](examples/monolithic_build_native/mldsa_native/mldsa_native_config.h)
8383
- [examples/multilevel_build/mldsa_native/mldsa_native_config.h](examples/multilevel_build/mldsa_native/mldsa_native_config.h)
8484
- [examples/multilevel_build_native/mldsa_native/mldsa_native_config.h](examples/multilevel_build_native/mldsa_native/mldsa_native_config.h)
85+
- [examples/restartable_sign/mldsa_native/mldsa_native_config.h](examples/restartable_sign/mldsa_native/mldsa_native_config.h)
8586
- [integration/liboqs/config_aarch64.h](integration/liboqs/config_aarch64.h)
8687
- [integration/liboqs/config_c.h](integration/liboqs/config_c.h)
8788
- [integration/liboqs/config_x86_64.h](integration/liboqs/config_x86_64.h)
@@ -133,6 +134,7 @@ source code and documentation.
133134
- [examples/monolithic_build_native/mldsa_native/mldsa_native_config.h](examples/monolithic_build_native/mldsa_native/mldsa_native_config.h)
134135
- [examples/multilevel_build/mldsa_native/mldsa_native_config.h](examples/multilevel_build/mldsa_native/mldsa_native_config.h)
135136
- [examples/multilevel_build_native/mldsa_native/mldsa_native_config.h](examples/multilevel_build_native/mldsa_native/mldsa_native_config.h)
137+
- [examples/restartable_sign/mldsa_native/mldsa_native_config.h](examples/restartable_sign/mldsa_native/mldsa_native_config.h)
136138
- [mldsa/mldsa_native.h](mldsa/mldsa_native.h)
137139
- [mldsa/mldsa_native_config.h](mldsa/mldsa_native_config.h)
138140
- [mldsa/src/ct.h](mldsa/src/ct.h)

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ EXAMPLE_DIRS := \
297297
examples/basic \
298298
examples/basic_deterministic \
299299
examples/basic_lowram \
300+
examples/restartable_sign \
300301
examples/monolithic_build \
301302
examples/monolithic_build_native \
302303
examples/monolithic_build_multilevel \

examples/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ See [basic_deterministic](basic_deterministic) for a basic example of how to bui
1616

1717
See [basic_lowram](basic_lowram) for a basic example of how to build a single instance of mldsa-native with reduced RAM usage (`MLD_CONFIG_REDUCE_RAM`). This is useful for embedded systems with tight RAM constraints.
1818

19+
## Restartable signing
20+
21+
See [restartable_sign](restartable_sign) for an example of how to bound the per-call signing time while staying FIPS 204 compliant, by making signing restartable: each call performs at most `MLD_CONFIG_MAX_SIGNING_ATTEMPTS` rejection-sampling iterations and, on exhaustion, the caller resumes the operation (via the `MLD_CONFIG_SIGN_HOOK_*` hooks). This is useful in timing-constrained environments.
22+
1923
## Multi-level build (C only)
2024

2125
See [multilevel_build](multilevel_build) for an example of how to build one instance of mldsa-native per security level,

examples/basic_deterministic/mldsa_native/mldsa_native_config.h

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -712,24 +712,127 @@
712712
/**
713713
* MLD_CONFIG_CONTEXT_PARAMETER
714714
*
715-
* Set this to add a context parameter that is provided to public
716-
* API functions and is then available in custom callbacks.
717-
*
718-
* The type of the context parameter is configured via
719-
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE.
715+
* Set this to add a caller-supplied context parameter to the public API
716+
* functions, which is then forwarded unchanged to the custom callbacks
717+
* (allocation, and the restartable-signing hooks below).
718+
*
719+
* When this option is set, every public API function gains a trailing
720+
* parameter
721+
*
722+
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE context
723+
*
724+
* as its last argument; its type is configured via
725+
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE (see below). mldsa-native treats this
726+
* value as opaque: it never dereferences it and only passes it on to the
727+
* configurable hook macros. It is meant to carry per-caller state -- e.g. a
728+
* pointer to a memory pool for the allocation hooks, or the resume state for
729+
* the restartable-signing hooks -- into those hooks.
730+
*
731+
* When this option is unset (the default), no extra parameter is added and
732+
* the hook macros never receive a context argument.
733+
*
734+
* Memory-allocation hooks (MLD_ALLOC / MLD_FREE):
735+
*
736+
* Internally, mldsa-native allocates and releases its large temporary
737+
* buffers via two macros, MLD_ALLOC(v, T, N, context) and
738+
* MLD_FREE(v, T, N, context), where `v` is the pointer variable to
739+
* declare/clear, `T` its element type, and `N` the element count. By
740+
* default these allocate `N` elements of type `T` on the stack and, on
741+
* free, zeroize the buffer. If MLD_CONFIG_CUSTOM_ALLOC_FREE is set, they
742+
* instead expand to the user-provided MLD_CUSTOM_ALLOC / MLD_CUSTOM_FREE,
743+
* forwarding `context` as the extra argument when (and only when)
744+
* MLD_CONFIG_CONTEXT_PARAMETER is set. This is the mechanism by which a
745+
* caller's allocator gains access to per-call state. See
746+
* MLD_CONFIG_CUSTOM_ALLOC_FREE for the macro signatures.
747+
*
748+
* Signing hooks (mld_sign_hook_resume / _attempt / _finish):
749+
*
750+
* The MLD_CONFIG_SIGN_HOOK_RESUME / _ATTEMPT / _FINISH options let signing
751+
* call into caller-provided functions around its rejection-sampling loop.
752+
* Each takes `context` only if MLD_CONFIG_CONTEXT_PARAMETER is also set (the
753+
* context is then its last argument); otherwise it takes no context. See
754+
* those options below for the full contract.
720755
*/
721756
/* #define MLD_CONFIG_CONTEXT_PARAMETER */
722757

723758
/**
724759
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE
725760
*
726-
* Set this to define the type for the context parameter used by
727-
* MLD_CONFIG_CONTEXT_PARAMETER.
761+
* Set this to define the type of the context parameter added by
762+
* MLD_CONFIG_CONTEXT_PARAMETER. It can be any C type usable as a function
763+
* parameter, e.g. `void *` or a pointer to a caller-defined struct such as
764+
* `struct my_ctx *`.
728765
*
729-
* This is only relevant if MLD_CONFIG_CONTEXT_PARAMETER is set.
766+
* This option must be defined if and only if MLD_CONFIG_CONTEXT_PARAMETER is
767+
* defined; defining one without the other is a compile-time error.
730768
*/
731769
/* #define MLD_CONFIG_CONTEXT_PARAMETER_TYPE void* */
732770

771+
/**
772+
* Signing hooks: MLD_CONFIG_SIGN_HOOK_RESUME / _ATTEMPT / _FINISH
773+
*
774+
* Three optional, independent hooks into the ML-DSA signing rejection-sampling
775+
* loop. Each is enabled by defining the matching option, in which case the
776+
* integration MUST provide the corresponding function. If a hook needs
777+
* per-operation state, enable MLD_CONFIG_CONTEXT_PARAMETER; the context is then
778+
* appended as the last argument. The functions must be callable for every
779+
* parameter set in the build.
780+
*
781+
* - MLD_CONFIG_SIGN_HOOK_RESUME: uint16_t mld_sign_hook_resume([ctxt])
782+
*
783+
* Returns the nonce (rejection-sampling counter) to resume from; 0 for a
784+
* fresh operation. This makes signing resumable: with a small
785+
* MLD_CONFIG_MAX_SIGNING_ATTEMPTS bound, each call performs at most that many
786+
* attempts and, on MLD_ERR_SIGN_ATTEMPTS_EXHAUSTED, the caller re-invokes
787+
* signing -- same message, sk and rnd -- to continue from the recorded nonce.
788+
* No attempt is repeated or skipped, so the result is the bit-for-bit FIPS
789+
* 204 signature with bounded per-call runtime, keeping a < 814 bound (which
790+
* is otherwise non-compliant) FIPS 204 compliant. Resuming is only correct if
791+
* the randomness is fixed across calls, so this option requires
792+
* MLD_CONFIG_NO_RANDOMIZED_API and is sound only for the deterministic
793+
* mld_sign_signature_internal / mld_sign_signature_extmu. It is also
794+
* incompatible with MLD_CONFIG_KEYGEN_PCT, whose one-shot internal signature
795+
* cannot be resumed: a consumer needing both must run their own (trivial)
796+
* PCT built from the public API.
797+
*
798+
* - MLD_CONFIG_SIGN_HOOK_ATTEMPT: void mld_sign_hook_attempt(nonce[, ctxt])
799+
*
800+
* Called before each attempt with the current nonce. Observe-only (logging,
801+
* benchmarking; a resumable integration may record nonce + 1 as the next
802+
* resume point) and compatible with any API.
803+
*
804+
* - MLD_CONFIG_SIGN_HOOK_FINISH: void mld_sign_hook_finish(nonce[, ctxt])
805+
*
806+
* Called on success with the succeeding nonce. Observe-only (a resumable
807+
* integration typically resets its stored nonce here) and compatible with
808+
* any API.
809+
*
810+
* When an option is unset, the hook defaults to a no-op (resume to 0), i.e.
811+
* ordinary one-shot signing.
812+
*
813+
* See examples/restartable_sign for a worked example using all three.
814+
*/
815+
/* #define MLD_CONFIG_SIGN_HOOK_RESUME
816+
#define MLD_CONFIG_SIGN_HOOK_ATTEMPT
817+
#define MLD_CONFIG_SIGN_HOOK_FINISH
818+
#if !defined(__ASSEMBLER__)
819+
#include <stdint.h>
820+
#include "src/sys.h"
821+
static MLD_INLINE uint16_t mld_sign_hook_resume(void)
822+
{
823+
... return the nonce to resume from ...
824+
}
825+
static MLD_INLINE void mld_sign_hook_attempt(uint16_t nonce)
826+
{
827+
... log the attempt; for resume, store nonce + 1 ...
828+
}
829+
static MLD_INLINE void mld_sign_hook_finish(uint16_t nonce)
830+
{
831+
... mark the operation complete (nonce = successful attempt) ...
832+
}
833+
#endif
834+
*/
835+
733836
/**
734837
* MLD_CONFIG_REDUCE_RAM
735838
*

examples/basic_lowram/mldsa_native/mldsa_native_config.h

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -711,24 +711,127 @@
711711
/**
712712
* MLD_CONFIG_CONTEXT_PARAMETER
713713
*
714-
* Set this to add a context parameter that is provided to public
715-
* API functions and is then available in custom callbacks.
716-
*
717-
* The type of the context parameter is configured via
718-
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE.
714+
* Set this to add a caller-supplied context parameter to the public API
715+
* functions, which is then forwarded unchanged to the custom callbacks
716+
* (allocation, and the restartable-signing hooks below).
717+
*
718+
* When this option is set, every public API function gains a trailing
719+
* parameter
720+
*
721+
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE context
722+
*
723+
* as its last argument; its type is configured via
724+
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE (see below). mldsa-native treats this
725+
* value as opaque: it never dereferences it and only passes it on to the
726+
* configurable hook macros. It is meant to carry per-caller state -- e.g. a
727+
* pointer to a memory pool for the allocation hooks, or the resume state for
728+
* the restartable-signing hooks -- into those hooks.
729+
*
730+
* When this option is unset (the default), no extra parameter is added and
731+
* the hook macros never receive a context argument.
732+
*
733+
* Memory-allocation hooks (MLD_ALLOC / MLD_FREE):
734+
*
735+
* Internally, mldsa-native allocates and releases its large temporary
736+
* buffers via two macros, MLD_ALLOC(v, T, N, context) and
737+
* MLD_FREE(v, T, N, context), where `v` is the pointer variable to
738+
* declare/clear, `T` its element type, and `N` the element count. By
739+
* default these allocate `N` elements of type `T` on the stack and, on
740+
* free, zeroize the buffer. If MLD_CONFIG_CUSTOM_ALLOC_FREE is set, they
741+
* instead expand to the user-provided MLD_CUSTOM_ALLOC / MLD_CUSTOM_FREE,
742+
* forwarding `context` as the extra argument when (and only when)
743+
* MLD_CONFIG_CONTEXT_PARAMETER is set. This is the mechanism by which a
744+
* caller's allocator gains access to per-call state. See
745+
* MLD_CONFIG_CUSTOM_ALLOC_FREE for the macro signatures.
746+
*
747+
* Signing hooks (mld_sign_hook_resume / _attempt / _finish):
748+
*
749+
* The MLD_CONFIG_SIGN_HOOK_RESUME / _ATTEMPT / _FINISH options let signing
750+
* call into caller-provided functions around its rejection-sampling loop.
751+
* Each takes `context` only if MLD_CONFIG_CONTEXT_PARAMETER is also set (the
752+
* context is then its last argument); otherwise it takes no context. See
753+
* those options below for the full contract.
719754
*/
720755
/* #define MLD_CONFIG_CONTEXT_PARAMETER */
721756

722757
/**
723758
* MLD_CONFIG_CONTEXT_PARAMETER_TYPE
724759
*
725-
* Set this to define the type for the context parameter used by
726-
* MLD_CONFIG_CONTEXT_PARAMETER.
760+
* Set this to define the type of the context parameter added by
761+
* MLD_CONFIG_CONTEXT_PARAMETER. It can be any C type usable as a function
762+
* parameter, e.g. `void *` or a pointer to a caller-defined struct such as
763+
* `struct my_ctx *`.
727764
*
728-
* This is only relevant if MLD_CONFIG_CONTEXT_PARAMETER is set.
765+
* This option must be defined if and only if MLD_CONFIG_CONTEXT_PARAMETER is
766+
* defined; defining one without the other is a compile-time error.
729767
*/
730768
/* #define MLD_CONFIG_CONTEXT_PARAMETER_TYPE void* */
731769

770+
/**
771+
* Signing hooks: MLD_CONFIG_SIGN_HOOK_RESUME / _ATTEMPT / _FINISH
772+
*
773+
* Three optional, independent hooks into the ML-DSA signing rejection-sampling
774+
* loop. Each is enabled by defining the matching option, in which case the
775+
* integration MUST provide the corresponding function. If a hook needs
776+
* per-operation state, enable MLD_CONFIG_CONTEXT_PARAMETER; the context is then
777+
* appended as the last argument. The functions must be callable for every
778+
* parameter set in the build.
779+
*
780+
* - MLD_CONFIG_SIGN_HOOK_RESUME: uint16_t mld_sign_hook_resume([ctxt])
781+
*
782+
* Returns the nonce (rejection-sampling counter) to resume from; 0 for a
783+
* fresh operation. This makes signing resumable: with a small
784+
* MLD_CONFIG_MAX_SIGNING_ATTEMPTS bound, each call performs at most that many
785+
* attempts and, on MLD_ERR_SIGN_ATTEMPTS_EXHAUSTED, the caller re-invokes
786+
* signing -- same message, sk and rnd -- to continue from the recorded nonce.
787+
* No attempt is repeated or skipped, so the result is the bit-for-bit FIPS
788+
* 204 signature with bounded per-call runtime, keeping a < 814 bound (which
789+
* is otherwise non-compliant) FIPS 204 compliant. Resuming is only correct if
790+
* the randomness is fixed across calls, so this option requires
791+
* MLD_CONFIG_NO_RANDOMIZED_API and is sound only for the deterministic
792+
* mld_sign_signature_internal / mld_sign_signature_extmu. It is also
793+
* incompatible with MLD_CONFIG_KEYGEN_PCT, whose one-shot internal signature
794+
* cannot be resumed: a consumer needing both must run their own (trivial)
795+
* PCT built from the public API.
796+
*
797+
* - MLD_CONFIG_SIGN_HOOK_ATTEMPT: void mld_sign_hook_attempt(nonce[, ctxt])
798+
*
799+
* Called before each attempt with the current nonce. Observe-only (logging,
800+
* benchmarking; a resumable integration may record nonce + 1 as the next
801+
* resume point) and compatible with any API.
802+
*
803+
* - MLD_CONFIG_SIGN_HOOK_FINISH: void mld_sign_hook_finish(nonce[, ctxt])
804+
*
805+
* Called on success with the succeeding nonce. Observe-only (a resumable
806+
* integration typically resets its stored nonce here) and compatible with
807+
* any API.
808+
*
809+
* When an option is unset, the hook defaults to a no-op (resume to 0), i.e.
810+
* ordinary one-shot signing.
811+
*
812+
* See examples/restartable_sign for a worked example using all three.
813+
*/
814+
/* #define MLD_CONFIG_SIGN_HOOK_RESUME
815+
#define MLD_CONFIG_SIGN_HOOK_ATTEMPT
816+
#define MLD_CONFIG_SIGN_HOOK_FINISH
817+
#if !defined(__ASSEMBLER__)
818+
#include <stdint.h>
819+
#include "src/sys.h"
820+
static MLD_INLINE uint16_t mld_sign_hook_resume(void)
821+
{
822+
... return the nonce to resume from ...
823+
}
824+
static MLD_INLINE void mld_sign_hook_attempt(uint16_t nonce)
825+
{
826+
... log the attempt; for resume, store nonce + 1 ...
827+
}
828+
static MLD_INLINE void mld_sign_hook_finish(uint16_t nonce)
829+
{
830+
... mark the operation complete (nonce = successful attempt) ...
831+
}
832+
#endif
833+
*/
834+
732835
/**
733836
* MLD_CONFIG_REDUCE_RAM
734837
*

0 commit comments

Comments
 (0)