[ideation] Secure Offchain Storage via Session Key Derivation and Zeroization

Context & Overview

During distributed key generation (DKG) and collective signing inside pallets/exodus, secret and public packages are stored in the offchain storage of the validator’s node for later use. For public packages, this isn’t important - but for secret ones, it could be potentially dangerous.

Possible leakage vectors:

  1. Some validators like to transfer the db/full folder from one machine to another for faster sync. During transfer, the packages could be stolen, or worse.
  2. There is an RPC call offchain_localStorage that gives direct access to the underlying storage. While the values are raw for now, if the port is exposed to the internet because of a misconfiguration - or to an unwanted sub-network (in a complex Kubernetes stack or some other multi-machine setup) - the secrets are visible right away.

Push Back

The main pushback here is the fact that if a malicious attacker can get direct access to the filesystem, there’s no remedy anyway — because the keystore folder with active session keys will be taken right away, so there’s no need to gather secrets from local storage. That’s why I’m questioning the whole idea: is it really necessary?

Solution

Because we already have ChaChaPoly1305 encryption in place in this pallet, it looks like a good option to reuse it for secrets in offchain storage. For that, the node would need some symmetric key that persists across OCW runs.

For now, the exodus pallet is using sr25519 as the scheme for the session key, and we are locked into the RuntimeAppPublic trait, which isn’t very flexible:

pub trait RuntimeAppPublic: Sized {
    type Signature: Debug + Eq + PartialEq + Clone + TypeInfo + Codec;
    type ProofOfPossession: Debug + Eq + PartialEq + TypeInfo + Clone;

    const ID: KeyTypeId;

    // Required methods
    fn all() -> Vec<Self>;
    fn generate_pair(seed: Option<Vec<u8>>) -> Self;
    fn sign<M: AsRef<[u8]>>(&self, msg: &M) -> Option<Self::Signature>;
    fn verify<M: AsRef<[u8]>>(
        &self,
        msg: &M,
        signature: &Self::Signature,
    ) -> bool;
    fn generate_proof_of_possession(
        &mut self,
        owner: &[u8],
    ) -> Option<Self::ProofOfPossession>;
    fn verify_proof_of_possession(
        &self,
        owner: &[u8],
        pop: &Self::ProofOfPossession,
    ) -> bool;
    fn to_raw_vec(&self) -> Vec<u8> ⓘ;
}

In order to fit the symmetric nature between OCW runs, we recommend changing the session key to ed25519, which can be used like this:

// PSEUDO CODE

const ENCRYPTION_PREFIX = "prefix-"

let message = prefix + authority_id.encode() + dkg_index + authority_index;
let signature = authority_id.sign(&message)?;
let symmetric_key = blake2_256(&signature.encode());

// Use it for ChaCha20Poly1305

Zeroization

Because it’s just good practice overall, it would be great to add zeroization for all secrets and the symmetric key itself. The recommendation is to use the zeroize crate - the same one already used as a dependency of the frost-core crate.

The Question

In any case, decryption and encryption will require more computation for each OCW. At the same time, leakage of the keystore will lead to decryption of the secrets too. In my opinion, the offchain_localStorage call and misconfiguration of the ports are the only real use cases.

A review of the proposal is welcome - if you have any thoughts, put them below.

1 Like