# Stronghold Procedure API
###### tags: `Stronghold` `Procedures` `Spec`
***Date**: October 2021*
_The procedure API offers infrastructure code to implement cryptographic procedures in a secure way. Is is meant as a safe wrapper around algorithms from [crypto.rs](https://github.com/iotaledger/crypto.rs), with an additional layer of abstraction that allows the user to build and execute complex procedures._
Procedures implement cryptographic operations that are performed on secrets in the vault. Procedures may create secrets or use existing secrets in their algorithms, but can never read them, or expose them outside of the guarded type. A secret is store as record in the vault at a specific `Location`.
:::info
### Terminology
- **Primitive Produres**: Procedures, that perform a single Cryptographic Algorithm, like the signing of a message with an Ed25519 private key.
- **Complex Procedure**: procedures that consist of multiple, sequentially executed Primitive Procedures.
- **Secret**: `Vec<u8>` that is stored as record inside a vault and never gets revealed.
- **Guard**: Wrapper from the Stronghold Runtime for guarding Secrets ([more info](https://github.com/iotaledger/stronghold.rs/blob/dev/engine/runtime/README.md)). Procedures only operate on Guards (=`GuardedVec`), not the plain Secrets.
- **Input** / **Output**: Non-secret data consumed / produced by a Procedure.
E.g. `Ed25519Sign`: `(message: Input + private_key: Guard) -> signature: Output`
:::
## Primitive Procedures
Each primitive procedure implements the `ProcedureStep` trait, with the `execute` method that includes the procedure's logic:
```rust
pub trait ProcedureStep {
fn execute<R: Runner>(self, runner: &mut R, state: &mut State)
-> Result<(), ProcedureError>;
}
```
The `Runner` serves as bridge to the underlying engine and allows secure access to the vault. It provides methods for using existing secret, deriving new secrets, writing to the vault or removing records. In the client, the `SecureClient` actor functions as `Runner`.
The `State` collects the (non-secret) output of each procedure in the `CollectedOutput`, that can be used by subsequent procedures in a chain, and is returned to the caller. Apart from that, the `State` writes a log of newly created records during execution, and whether they are temporary or not. Temporary records are always revoked and garbage collected after the procedure execution finishes.
**Note**: Procedures are treated as **atomic**. If one procedure step in a complex procedure fails, the change-log is used to delete all newly created records.
```rust
pub struct State {
aggregated_output: HashMap<OutputKey, (ProcedureIo, bool)>,
change_log: Vec<ChangeLog>,
}
```
### Procedure types
For the primitive procedures provided in Stronghold, the `ProcedureStep` is not implemented directly. Instead, Procedures are categorized into 4 different traits, and macros are used to implement the necessary boilerplate code. The categories are characterized by whether a Procedures uses an existing Secret and / or writes a new one into the Vault.
Trait | Use | Write | Pseudo Signature | Example
-----------------|-------|-------|----------------------------------------|----------------
`GenerateSecret` | false | true | `Input -> (Output, NewSecret)` |`Slip10Generate`
`DeriveSecret` | true | true | `(Input,Guard) -> (Output, NewSecret)` |`Slip10Derive`
`UseSecret` | true | false | `(Input, Guard) -> Output` |`Ed25519Sign`
`ProcessData` | false | false | `Input -> Output` |`Sha256Hash`
### Specifying Parameters and Products
All procedures that use an existing secret require that the location of this secret (=`source`) is provided. If a procedure is writing a new secret into the vault, the `target` location is per default a random location, that may be used by subsequent procedures as source, but will be revoked after the procedure's execution. For permanent secrets, the `PersistSecret` trait is implemented for all `GenerateSecret` and `DeriveSecret` types, that allows to specify the permanent target location:
```rust
pub trait PersistSecret {
/// The location in the vault to which the secret will be written.
/// If [`PersistSecret::write_secret`] was not explicitly set, this is a random location
/// and the new record will be revoked after the procedure's execution.
///
/// This location can be used as input location for other procedure.
fn target(&self) -> Location;
/// Permanently store the new secret in the vault and the set location.
fn write_secret(self, location: Location, hint: RecordHint) -> Self;
}
```
E.g. in case of `Slip10Generate -> Slip10Derive` where the newly generated seed should be used to derive a new child keypair:
```rust
// Generate a new, temporary seed.
let generate = Slip10Generate::default();
// Use previous target location as source location.
// Store the new private key permanently.
let derive = Slip10Derive::new_from_seed(generate.target(), ..).write_secret(..);
// Execute the procedures
stronghold.runtime_exec(generate.then(derive)).await
```
A procedure's output is written into a `TempCollectedOutput` with a specific `OutputKey`. The same key may be used by subsequent procedures to use the output as input.
The input for a procedure (if there is any) is set in the `InputData` enum:
```rust
pub enum InputData<T> {
// Use the output from a previous procedure as input.
Key(OutputKey),
// Use a fixed input
Value(T),
}
```
For the same procedure, either an `OutputKey`, or a value of Type `Input` can be set as Parameter:
```rust
let hash_fixed = Hash::<Blake2b256>::new(Vec::from("this is a fixed message"));
let hash_dynamic = Hash::<Blake2b256>::new(previous_procedure.output_key());
```
The Input type must be convertible from / into the `ProcedureIo` type, which is implemented for `Vec<u8>`, `String`, `[u8; N]` and `GenericArray<u8; N>`.
Analogous to newly created secrets, a procedure's output is only temporary stored at a random `OutputKey`. The `OutputInfo` trait is implemented for all Procedures that return any non-secret output (if there is no Input / Output, the associated type is `= ()` in the `GenerateSecret` / `DeriveSecret` / .. trait). `PersistOutput` allows specifying a permanent location and to get the current output location:
```rust
pub trait PersistOutput {
/// [`OutputKey`] with which the procedure's output is associated.
/// This key can be used to specify the input of subsequent procedures in a procedure chain.
///
/// If [`PersistOutput::store_output`] was not set, this key is random, and the output will not be
/// persisted in the [`CollectedOutput`] that is returned to the user.
fn output_key(&self) -> OutputKey;
/// Write the procedure's non-secret output into the [`CollectedOutput`] that is returned to the user.
fn store_output(self, key: OutputKey) -> Self;
}
```
When a procedure successfully executed, the `TempCollectedOutput` is converted to the `CollectedOutput` that only includes the permanently stored outputs.
```rust
// Create sign procedure with temp output.
let sign = Ed25519Sign::new(Vec::from("my message"), ..);
// Key for storing the hashed output.
let key = OutputKey::new("digested");
// Hash the previously signed message and store it at the specified key.
let hash = Hash::<Sha256>::new(sign.output_key()).store_output(key);
// Execute the procedures.
//
// The collected output only includes the output of the Hash procedure.
let mut collected_output = stronghold.runtime_exec(sign.then(hash)).await?;
// Take the `ProcedureIo` and convert it to the correct type.
let digested: Vec<u8> = collected_output.take(key)?;
```
## Complex Procedures
Every primitive procedure implements `Into<PrimitiveProcedure>`, with `PrimitiveProcedure` being an enum that wraps all supported procedures.
A complex procedure `Procedure` is essentially a wrapper around a `Vec<PrimitiveProcedure>`, that itself implements `ProcedureStep` by executing all primitive Procedures from the vector sequentially:
```rust
pub struct Procedure {
inner: Vec<PrimitiveProcedure>,
}
impl ProcedureStep for Procedure {
fn execute<R: Runner>(self, runner: &mut R, state: &mut State)
-> Result<(), ProcedureError>
{
self.inner.into_iter().try_for_each(|p| p.execute(runner, state))
}
}
impl Message for Procedure {
type Result = Result<CollectedOutput, ProcedureError>;
}
```
Complex procedures are created with the `then` method that is auto-implemented in the `ProcedureStep` trait:
```rust
pub trait ProcedureStep {
fn execute<R: Runner>(self, runner: &mut R, state: &mut State)
-> Result<(), ProcedureError>;
fn then<P>(self, next: P) -> Procedure
where
Self: Into<Procedure>,
P: Into<Procedure>,
{
let mut procedure = self.into();
procedure.inner.extend(next.into().inner);
procedure
}
}
```
`Into<Procedure>` is implemented for all primitive procedures.
## Example
### Implement a primitive Procedure
Example for a primitive Procedure:
```rust
#[derive(Procedure)]
pub struct Slip10Derive {
// Config parameters.
chain: Chain,
parent_ty: Slip10ParentType,
// Auto-implement `OutputInfo` trait.
#[output_key]
output_key: TempOutput,
// Auto-implement `SourceInfo` trait.
#[source]
source: Location,
// Auto-implement `TargetInfo` trait.
#[target]
target: TempTarget,
}
impl Slip10Derive {
pub fn new_from_seed(seed: Location, chain: Chain) -> Self {
Self::new(chain, seed, Slip10ParentType::Seed)
}
pub fn new_from_key(parent: Location, chain: Chain) -> Self {
Self::new(chain, parent, Slip10ParentType::Key)
}
fn new(chain: Chain, source: Location, parent_ty: Slip10ParentType) -> Self {
Slip10Derive {
parent_ty,
chain,
source,
target: TempTarget::default(),
output_key: TempOutput::default(),
}
}
}
// Implement the procedure's logic.
//
// The macro generates the ProcedureStep trait with the necessary code
// for fetching the guard from the runner, and for writing the Products
// into the TempCollectedOutput and Vault.
#[execute_procedure]
impl DeriveSecret for Slip10Derive {
// Non-secet input.
type Input = ();
// Non-secret output.
type Output = ChainCode;
fn derive(self, _: Self::Input, guard: GuardedVec<u8>)
-> Result<Products<ChainCode>, engine::Error>
{
// Derive Keypair
let dk = match self.source {
SLIP10DeriveInput::Key(_) => {
slip10::Key::try_from(&*guard.borrow())
.and_then(|parent| parent.derive(&self.chain))
}
SLIP10DeriveInput::Seed(_) => {
slip10::Seed::from_bytes(&guard.borrow())
.derive(slip10::Curve::Ed25519, &self.chain)
}
}?;
Ok(Products {
// Write to the vault into the target Location.
secret: dk.into(),
// Written to the CollectedOutput to the output-key.
output: dk.chain_code(),
})
}
}
```
The macro would generate the following code:
```rust
impl OutputInfo for Slip10Derive {..}
impl SourceInfo for Slip10Derive {..}
impl TargetInfo for Slip10Derive {..}
impl ProcedureStep for Slip10Derive {
fn execute<X: Runner>(
self,
runner: &mut X,
state: &mut State,
) -> Result<(), ProcedureError> {
// Location for fetching the source guard.
let location_0 = <Self as SourceInfo>::source_location(&self).clone();
// Target into which the new secret is written.
let TempTarget {
write_to:
Target {
location: location_1,
hint,
},
is_temp: is_secret_temp,
} = <Self as TargetInfo>::target_info(&self).clone();
// If there was any input, it would be fetched here
// via the InputInfo trait.
let input = ();
// Key for non-secret output.
let TempOutput {
write_to: key,
is_temp: is_out_data_temp,
} = <Self as OutputInfo>::output_info(&self).clone();
// Execute Procedure on the runner
let f = move |guard| <Self as DeriveSecret>::derive(self, input, guard);
let output = runner
.exec_proc(&location_0, &location_1, hint, f)
.map_err(ProcedureError::VaultError)?;
// Write changelog for new record.
state.add_log(location_1, is_secret_temp);
// Convert output into ProcedureIo, write into TempCollectedOutput
state.insert_output(key, output.into_procedure_io(), is_out_data_temp);
Ok(())
}
}
```
### Build and execute complex procedure
The below code exemplifies a complex procedure that generates a new permanent seed, derives a temp keypair, and uses this keypair to sign a message.
```rust
async fn usecase_ed25519_as_complex() {
let stronghold = setup_stronghold().await;
let msg = "Test message";
// Permanent location for the seed, for future usage.
let seed_location = Location::generic(vec![1u8],
// Keys for returning the public key and signed message.
let pk_result = OutputKey::new("public key")
let sign_result = OutputKey::new("signed message");
vec![1u8]);
// Generate a new permanent seed and write it to
// the specified location.
let generate = Slip10Generate::default()
.write_secret(seed_location.clone(), RecordHint::new("seed"));
// Derive a temporary keypair from the new seed.
let derive = Slip10Derive::new_from_seed(generate.target(), fresh::hd_path().1);
// Get the public key of the new keypair.
let get_pk = Ed25519PublicKey::new(derive.target())
.store_output(pk_result.clone());
// Sign the test message with the new keypair.
let sign = Ed25519Sign::new(msg.clone(), derive.target())
.store_output(sign_result.clone());
// Create a complex procedure that chains the primitive procedures.
let complex_proc = generate.then(derive).then(get_pk).then(sign);
// Execute the complex procedure with all steps.
let mut output = sh.runtime_exec(complex_proc).await.unwrap()
// Get the public key from the output.
let pub_key_vec: [u8; PUBLIC_KEY_LENGTH] = output.take(&pk_result).unwrap();
let pk = PublicKey::try_from_bytes(pub_key_vec).unwrap();
// Get the signed message from the output.
let sig_vec: [u8; SIGNATURE_LENGTH] = output.take(&sign_result).unwrap();
let sig = Signature::from_bytes(sig_vec);
// Verify the signature.
assert!(pk.verify(&sig, &msg));
}
```