Add interruptible Crypto API operations - #375
Conversation
gilles-peskine-arm
left a comment
There was a problem hiding this comment.
I've reviewed the document with the partial implementation in TF-PSA-Crypto in mind. We know that we'll need to update our implementation since it followed a now outdated beta.
Many of my comments apply to multiple operation types. I only noted the first place where I noticed something.
My biggest concern is the verify flow. For verify-message, I don't think providing the signature before the message works out in practice.
| Identifier of the key to use for the operation. It must be an asymmetric key pair or asymmetric public key. The key must either permit the usage `PSA_KEY_USAGE_VERIFY_HASH` or `PSA_KEY_USAGE_VERIFY_MESSAGE`. | ||
| .. param:: psa_algorithm_t alg | ||
| An asymmetric signature algorithm: a value of type `psa_algorithm_t` such that :code:`PSA_ALG_IS_SIGN(alg)` is true. | ||
| .. param:: const uint8_t * signature |
There was a problem hiding this comment.
Can you remind me why the signature is passed during setup, rather than after the message?
Streaming protocols often send the message before the signature to verify, so in practice, this is likely to be a burden if the algorithm doesn't follow the sign-the-hash paradigm.
There was a problem hiding this comment.
That limits the algorithm selection: PureEdDSA, SLH-DSA, LMS, and XMSS all require the randomisation element from the signature as part of the hash prefix before the message content.
There was a problem hiding this comment.
Deferred-signature verification support has been added
|
|
||
| .. _interruptible-operations: | ||
|
|
||
| Interruptible operations |
There was a problem hiding this comment.
This section could use a subsection that discusses op counts:
xxx_iop_get_num_ops()functions (still meaningful aftercomplete()returnsPSA_SUCCESS).psa_iop_set_max_ops()and a brief statement that ops do not have a fixed meaning.
There was a problem hiding this comment.
This is currently present in lines 252-256. Is there more that should be said here?
There was a problem hiding this comment.
The section introduces the concepts, but I think it should be a bit more concrete. At the very least, it should mention the get_num_ops naming convention.
Quick response on this:
A algorithm-agnostic flow requires the signature before the message parts. |
Signed-off-by: Andrew Thoelke <andrew.thoelke@arm.com>
Signed-off-by: Andrew Thoelke <andrew.thoelke@arm.com>
Signed-off-by: Andrew Thoelke <andrew.thoelke@arm.com>
…ions Signed-off-by: Andrew Thoelke <andrew.thoelke@arm.com>
|
Following discussion - we need to support use cases where the signature is only provided after the message content (required for some streaming protocols), as well as use cases where the signature is provided before the message (which is required for some algorithms). For Using the current For the new flow, three additional APIs are provided:
This design, providing a distinct setup function, enables the implementation to detect the application intent explicitly and immediately, and respond with an error if not supported. With a more generic, single setup function without a signature; the application intent can only be inferred when |
Signed-off-by: Andrew Thoelke <andrew.thoelke@arm.com>
|
I think I've managed to address all of the substanive issues raised in the feedback so far. I would appreciate a re-review of the resulting updated PR. |
gilles-peskine-arm
left a comment
There was a problem hiding this comment.
Thanks for the updates. I only have a couple of minor nits now, except maybe for one point. I want to think further about verification with and without deferred signature.
| .. retval:: PSA_ERROR_BAD_STATE | ||
| The following conditions can result in this error: | ||
|
|
||
| * The operation state is not valid: setup must be complete, and no call to `psa_sign_iop_set_context()`, `psa_sign_iop_hash()`, `psa_sign_iop_update()`, or `psa_sign_iop_complete()` has been made. |
There was a problem hiding this comment.
There's still an ambiguity here in the second clause, where it's not clear whether the clause describes what caused the error or what must be done to avoid the error. I think it would be simpler to understand if error descriptions always described what causes the error.
| * The operation state is not valid: setup must be complete, and no call to `psa_sign_iop_set_context()`, `psa_sign_iop_hash()`, `psa_sign_iop_update()`, or `psa_sign_iop_complete()` has been made. | |
| * The operation state is not valid: setup must be complete, and no call to `psa_sign_iop_set_context()`, `psa_sign_iop_hash()`, `psa_sign_iop_update()`, or `psa_sign_iop_complete()` must have been made. |
or
| * The operation state is not valid: setup must be complete, and no call to `psa_sign_iop_set_context()`, `psa_sign_iop_hash()`, `psa_sign_iop_update()`, or `psa_sign_iop_complete()` has been made. | |
| * The operation state is not valid: the operation is inactive, setup is incomplete, or a call to `psa_sign_iop_set_context()`, `psa_sign_iop_hash()`, `psa_sign_iop_update()`, or `psa_sign_iop_complete()` has been made. |
(I haven't made a list of where this pattern occurs.)
There was a problem hiding this comment.
This could be widespread - I think in all the MP operations, the 'The operation state is not valid:...' conditions for BAD_STATE go on to define what 'valid' means for the function. The statement here mixes requirement tone - 'setup must be complete' - with present tense state tone - 'no call to ... has been made'.
The second alternative is tempting, with the description actually presents the states that are invalid. I would introduce it as 'The operation state is invalid: ...' - this removes the [existing] ambiguity as to whether the statement that follows is a definition of 'valid states' or of 'not valid states'?
For consistency, this might be a change we should make across all operation functions? - which goes well beyond the remit of this PR. But we could do that for these interruptibles, and follow with a spec-wide edit for the existing multi-parts?
There was a problem hiding this comment.
I haven't reviewed the BAD_STATE descriptions recently. Going by the ones in this PR, the simple ones are ok either way, it's the ones that are half this-causes-an-error and half how-to-avoid-an-error that are confusing. But I would be in favor of aligning everything to this-causes-an-error (in this PR for the new functions, and separately for existing ones), even the ones that aren't confusing on their own.
There was a problem hiding this comment.
I've addressed this issue for this PR, and created #381 to rework these clauses for the existing multi-part operations.
| * A successful call to `psa_generate_key_iop_complete()`. | ||
| * A call to `psa_generate_key_iop_abort()`. | ||
|
|
||
| If `psa_generate_key_iop_start()` returns an error, the operation object remains inactive, but its number of *ops* can be reset to zero. |
There was a problem hiding this comment.
This still isn't fully accurate: if psa_xxx_iop_start() is called on an already active object, it's a BAD_STATE error, but the object doesn't “remain inactive”.
There was a problem hiding this comment.
Quite right, but 'ouch!':
-
This affects all the interruptibles in this PR. The statement is only true if the operation was inactive prior to the call.
-
All existing multi-part operations suffer from a differently word defect of the same category. They state:
If
psa_xxx_setup()returns an error, the operation object is unchanged.Which is also only true if the operation was inactive to start with.
We changed the wording for the interruptibles, because the ops count can be changed by the call. But we have missed the 'If the operation was not inactive, psa_xxx_setup() returns PSA_ERROR_BAD_STATE, otherwise ...` that should preceed the current statements for all of these APIs.
There was a problem hiding this comment.
Looks like this warrants an issue to address this omission across all multi-part operations.
There was a problem hiding this comment.
Are you going to keep the requirement that a failed setup doesn't change the object?
This requirement was added in 1.1, and TF-PSA-Crypto hasn't caught up with it: setup() on an already active multipart operation aborts the operation. We don't mind the requirement from 1.1, but I only noticed very recently and it's low on our priority list.
There was a problem hiding this comment.
No, I don't think we ever meant for a call to setup having no effect if the object was active - that should always put the object into an error state, as for any other out-of-sequence call for an active operation.
The current wording was intended to make it clear that a failed setup [on an inactive operation] does not require a call to abort() to reset the operation - but the wording was too broad and we never noticed.
I think these new APIs, and the existing multi-part setup functions need to be reworked to state something along the lines of:
If
psa_xxx_setup()returnsPSA_ERROR_BAD_STATEthe operation enters an error state [, and will need to be aborted]. If any other error is returned, the operation object {is unchanged|remains inactive, ...}.
The first sentence mirrors what other operation functions state regarding any error response; the second is a qualified form of the current text for the setup/start functions.
There was a problem hiding this comment.
Addressed this for the interruptible APIs (see 63f6c68) - borrowing more of the pattern from the existing multi-part setup functions.
There was a problem hiding this comment.
I've also created #380 to address this for the existing multi-part operations.
| .. retval:: PSA_SUCCESS | ||
| Success. | ||
| The interruptible operation must now be completed by calling `psa_generate_key_iop_complete()`. | ||
| .. retval:: PSA_ERROR_ALREADY_EXISTS |
There was a problem hiding this comment.
I definitely don't want to implement a key id reservation in TF-PSA-Crypto. I wouldn't mind if reservation was forbidden. But there may be lower-level implementations where persistent key slots are tied more directly to physical addresses where reserving makes more sense, so I'm ok with allowing it in the specification.
|
|
||
| .. _interruptible-operations: | ||
|
|
||
| Interruptible operations |
There was a problem hiding this comment.
The section introduces the concepts, but I think it should be a bit more concrete. At the very least, it should mention the get_num_ops naming convention.
|
I had another thought about the verification and deferred signatures. For both the multi-part and interruptible verification operations, the operation object wil have to copy some or all of the signature data if it is passed during setup. For PQC signatures, this can be a very sizeable buffer and will be challenging for constrained implementations with no dynamic allocation. However, although it might work (from a memory management point of view) to have the application deal with the signature memory issue, and pass the signature to both the setup and the finishing phases for an algorithm that requires the signature before the message - is there a security/cryptographic risk in having the application pass what is meant to be the same signature data twice to the implementation? |
I think that for interruptible operations, this memory has to exist anyway between For multipart operations, it's a different matter: with most algorithms, the signature only needs to exist during the verify-finish phase, which is a single function call.
It's definitely more error prone. I can't think of a direct security risk however, since the inputs of verification are generally not confidential and the output is a single boolean. If the application makes a mistake and passes a different signature, the worst that can happen is that it will get a passing verification without a correct signature. But the application is only harming itself. The surface against glitch attacks is likely much higher, though. |
|
Another inconsistency has come to light after implementing #381. The behaviour of the interruptible operation functions when the operation is not in a valid state for the chosen algorithm is to report a These are scenarios where the function being called is incorrect for the operation state (including algorithm-specific state) irrespective of the arguments passed to the function. In other multi-part operations, similar scenarios result in a
I suspect the behaviour (using There are some scenarios in the new interruptible operations (and possibly in the multi-part sign/verify operations?) which might be better served by using |
|
Another unusual aspect to the sign and verify iop API is that the permission check for the keys is deferred until the application calls the This is not ideal, both from an application error discovery point of view, or from an implementation point of view (which often combines key lookup with policy verification). I propose to check for the appropriate SIGN_MESSAGE/VERIFY_MESSAGE usage when the operation is set up - noting that SIGN_HASH usage always implies SIGN_MESSAGE permission as well. Then there is a second permission check if the applicaiton uses the Although this still defers part of the permission check in some uses, it provides a better compromise without introducing separate setup APIs for hash and message interruptible signature operations. |
Signed-off-by: Andrew Thoelke <andrew.thoelke@arm.com>
|
I have added a few more commits to regularise the following:
|
gilles-peskine-arm
left a comment
There was a problem hiding this comment.
Mostly LGTM, but there are still a few kinks in the verify flow.
|
|
||
| If this function returns an error status, the operation enters an error state and must be aborted by calling `psa_verify_iop_abort()`. | ||
|
|
||
| .. function:: psa_verify_iop_set_signature |
There was a problem hiding this comment.
I'm not sure I agree with c91bbec. Why can't the function that receives the signature signal an error if it can already tell that the signature is invalid? For example, suppose psa_verify_iop_setup_start() or psa_verify_iop_set_signature() is called with a signature that is larger than what it can copy into the operation object. The only thing it can now do is to set an “invalid signature” flag in the operation object, which needs to stay until the call to psa_verify_iop_complete(), possibly past psa_verify_iop_update() calls. Or suppose that psa_verify_iop_setup_start() is called with an algorithm where the signature contains a nonce which needs to be hashed before the message, but the signature is too short and the implementation can't extract a nonce: what is psa_verify_iop_update() supposed to do?
There was a problem hiding this comment.
This change was done to align with the behaviour of multi-part asymmetric verification (in v1.5 - see psa_verify_setup()).
I think we went in that direction to simplify application code so that there is a single location where a signature mismtach is erported (and possibly to reduce the application-level differences in control flow when corrupted signatures are presented?).
However, I can understand that it requires additional implementation code/state to handle what are clearly invalid signatures, whilst appearing to permit the continuation of the operation - particularly when the signature is defined to include a nonce for the message processing...
There was a problem hiding this comment.
Decision: INVALID_SIGNATURE is permitted at the point the signature is provided when it is structurally invalid and cannot be used to perform the verification.
|
|
||
| * The operation is not active. | ||
| * The operation setup has not completed successfully. | ||
| * `psa_verify_iop_hash()`, `psa_verify_iop_set_signature()`, or `psa_verify_iop_complete()` has already been called. |
There was a problem hiding this comment.
psa_verify_iop_setup_start() supports algorithms where passing the signature can be deferred. This means that the operation object must have room to store the signature while the operation is ongoing. Then I don't see why psa_verify_iop_set_signature() cannot be called before supplying the message.
We may want to allow the implementation to overlap the signature storage with the message hash state in the operation object, and perhaps even to store only part of the signature (if the algorithm allows a first part of the verification to be done quickly, and a second part to be done more slowly but with less long-term memory). But if so, the non-deferred psa_verify_iop_setup_start() must reject algorithms that support deferred signature.
There was a problem hiding this comment.
The intention here is good:
- The early signature API is algorithm-agile
- The late signature API is necessary for streaming use cases
So this is good for applications. But the implementation cost for the former can be substantial, as there is no room for space optimization. A couple of options:
- Make the API split stronger - require that algorithms that support deferred signatures can only be used with the late-signature APIs.
- Make the implementation support decision more evident - explicitly list something like 'implementation does not support use of early-signature API with this algorithm' under
PSA_ERROR_NOT_SUPPORTED.
The former makes it explicit that for algorithm agility, an application has to code for both flows depending on the algorithm and the SUPPORT macro response. The latter suggests that algorithm-agile applications might need to do that for full portability across implementations.
There was a problem hiding this comment.
Decision: For the IOP, the implementation will have to store some/all of the signature for some of the interruptible operation lifetime. Thus requiring the implementation support the signature being provided before the message for all supported algorithms is acceptable for the implementations, and better for application design.
The approach for the multi-part message verify operation will be different, as the non-interruptible nature changes the implementation implications.
|
|
||
| The value is undefined if the operation object has not been initialized. | ||
|
|
||
| .. function:: psa_verify_iop_setup_start |
There was a problem hiding this comment.
Alternative proposal for verification:
psa_verify_iop_setup_start()does not take a signature argument.- There is no separate function
psa_verify_iop_setup_deferred_signature_start(). - If the algorithm supports deferred signature verification, you may call
psa_verify_iop_set_signature()at any time aftersetup_start()and beforecomplete(). - If the algorithm does not support deferred signature, you must call
psa_verify_iop_set_signature()beforepsa_verify_iop_setup_complete().
This feels to me like it's a little easier to both use and implement.
It means the operation object must have room for the signature, but that's already the case.
There was a problem hiding this comment.
This was an option we did discuss - and it does look neater as an API design.
This approach means that:
- The signature is never required to complete the setup for any verification algorithm.
- We are happy with reporting
PSA_ERROR_BAD_STATEfrompsa_verify_iop_update()- becausepsa_verify_iop_set_signature()has not been called [as required for the algorithm] - being the first indiciation to an application that their algorithm is not compatible with the call flow. (Obviously, the app should have used the support macro to check for this...)
There was a problem hiding this comment.
If we went down this route instead, then I'd be inclined to change the position w.r.t. whether an early signature is always acceptable. I would explicitly document that an agile application must use the macro to select between supplying the signature early vs supplying the signature late.
There was a problem hiding this comment.
Ooops I misread the proposal. (1) is not true - for early signatures @gilles-peskine-arm suggested that the signature must be provided before setup_complete().
For the IOP, a separate set_signature() function looks tidier as a new API, and due to the interruptible behaviour the implementation will have to store significant parts of the signature for some of the lifetime of the operation. In comparison, the multi-part message verification API (#379) benefits significantly from the deferred signature being passed in the finish() function.
Decision: Change the API design to have a single setup_start() function without signature, and a set_signature() function that can be called late for appropriate algorithms.
This is a rebased version of #107 and #199, targetting version 1.6 of the specification.
Apart from adding the previously drafted APIs, this incorporates other changes to the documentation since the original PRs. The introductory text for interruptible signatures has also been updated to reflect the presence of both multi-part signatures and interruptibel signature APIs.