Adding operations to Parsec
This mini how-to will cover how to add operations to the Parsec service and Rust client. The steps for adding a new provider operation will differ between providers, this guide will cover Mbed Crypto. The steps for adding operations to Parsec, Rust interface, and Rust client should be the same for all backend providers.
Operation specification
parsec-book
Create the specification page for the Parsec Book, under Operations (can also be found in the menu on the left of all pages). Include operation details such as parameters, results, errors specific to the operation, a description, and a link to the protobuf contract (which will be created further on). Also update all API tables on the Parsec Operations Coverage page to reflect the new addition (create any API tables if they are missing).
The opcode used should be the next available one. To find this, go to
parsec-interface-rs/src/requests/mod.rs
and find the largest in-use opcode.
parsec-operations
Create the protobuf
contracts in
parsec-operations
.
These are the contracts that client libraries use to communicate with the Parsec service.
Rust Interface
parsec-interface-rs
Create the Rust interfaces for the operation. These interfaces go between
protobuf
and the Parsec service on the service side, and the Rust
client library and protobuf on the client side. Take care when deciding on datatypes. Sensitive data
should be wrapped in either
Zeroizing
or
Secret
(choose depending on the
sensitivity of the data. Favor
Zeroizing
unless there is a clear
requirement for Secret
) to ensure
they are wiped from memory when dropped and should not have any output for debugging. The operation
definitions are stored in
src/operations/
.
A validate
method can be added to allow easy validation of the properties of the operations, to
check if there are any conflicting options, or combinations of options that are invalid. Tests must
be added for each operation with a validate method, to check it catches all erroneous combinations
of operation, and passes valid combinations.
A converter then needs to be added to
src/operations_protobuf
.
This will convert to and from the protobuf contract and the Rust interface. There should be four
methods per operation; two for going to and from the operation struct, and two for going to and from
the result struct. Again, tests need to be added to ensure all conversions happen correctly.
New protobuf operations need to be added to the parsec-operations
submodule - do this by entering
the submodule, then fetching and checking out the new contracts. Protobuf-generated code needs to be
committed in the repository to speed up the builds and remove some of the dependencies. You
therefore need to run a build using the regenerate-protobuf
feature enabled and then to identify
the generated Rust file in the correct directory, e.g.
target/debug/build/parsec-interface-fdbf7d7248ab4615/out/
. The file must be copied to
src/operations_protobuf/generated_ops/
and enabled as a module in mod.rs
. If your Rust operation
and/or result interfaces do not contain any sensitive information, add them to the
empty_clear_message!
block. Otherwise, implement
ClearProtoMessage
for the interface/s that contain sensitive information and
zeroize
any of the sensitive fields.
In
src/requests/mod.rs
,
add the opcode that was specified in the parsec-book
for the operation. Finally,
add the relevant
NativeOperation
and
NativeResult
entries, along with the mappings to the opcode from the
NativeOperation
and
NativeResult
to the Opcode. Then, add From
implementations to NativeOperation
and NativeResult
.
Finally, add the mod
declaration in
src/operations_protobuf/mod.rs
,
along with the entries in body_to_operation
, operation_to_body
, body_to_result
and
result_to_body
.
Parsec Rust client
parsec-client-rust
Add the user facing methods to
src/core/basic_client.rs
.
These are what users interacting with Parsec using Rust will use as the entrypoint. They are also
what Parsec’s e2e test
suit uses to test
Parsec, which is why this step comes before extending Parsec. Add the relevant tests to
src/core/testing/core_tests.rs
.
Other clients (e.g. Go)
The procedure for adding operations to another client library should be similar. We encourage that all clients should be updated to allow the new operation is as many languages as possible.
Parsec
psa-crypto-sys
Locate the parts of the PSA API in Mbed Crypto that will need to be used during the operation you are adding. This includes all constants, datatypes, macros and functions. Note: Mbed Crypto does not yet fully conform to the PSA API 1.0 specification, so there may be inconsistencies between the PSA documentation and the macros and functions that Mbed Crypto exposes.
Starting in
psa-crypto-sys
,
constants are added to
constants.rs
.
Inline functions and macros require a shim. Add the inline function and macro definitions into
shim.h
,
shim.c
and
shim.rs
.
When adding entries to
shim.rs
,
take note of where you place
unsafe
.
If the PSA API specification states that
under any circumstance, an unspecified result can be returned, then mark the entire fn
definition
as unsafe
. Otherwise, if the return values are always specified, wrap the call to the shim method
as unsafe
. Regular functions are added to pub use psa_crypto_binding::{ … }
.
psa-crypto
Define a Rust native interface for the operation you are adding in
src/operations
,
along with any Rust native types you need in
src/types
.
Generally, any helper macro functions are placed in the impl
block of the Rust native type they
are for.
The interface will work between the Rust native datatypes given to it and the C bindings in
psa-crypto-sys
. It is important to ensure that the Rust interface handles all
possible situations (e.g. closing a key-handle on an error) to ensure it is a safe Rust native
interface to the operation.
At this point, you will now have a safe Rust interface for the PSA operation you added.
parsec
Add the new operation to the correct
provider (in this case, Mbed
Crypto) as a
psa_xxx_internal
method. The operation method should take the user inputs, arrange them in a way
that psa-crypto
accepts, and provide any extra logic or storage if required (e.g.
an output buffer). The external implementation is to be added to the provider’s
mod.rs
file, which outputs a trace entry and passes the call back to the internal implementation.
A default implementation is added to
src/providers/mod.rs
that is used when a provider does not support a particular operation. It outputs a trace entry and
returns an error, stating that the operation is not supported. The external implementation in the
provider’s mod.rs
overrides this.
Add the NativeOperation
mapping in
src/back/backend_handler.rs
execute_request
function so Parsec can call the correct operation method when it receives a
request.
Finally, add end to end (e2e)
testing for the
new operation. Operations are added to
e2e_tests/src/lib.rs
to save from code duplication in the tests. This also makes it quick and easy to write tests, and to
see exactly what operations a test is performing. Test should be very thorough, taking the operation
through valid and invalid inputs, checking that the proper output is returned. If possible, external
data and crates should be used during testing. E.g. if testing the cryptographic operation, generate
keys and an encrypted message externally and test that Parsec can correctly decrypt the message.
(Note: if crates are added solely for testing purposes, add them to [dev-dependencies]
in
Cargo.toml
)