oasis_runtime_sdk/
callformat.rs

1//! Handling of different call formats.
2use std::convert::TryInto;
3
4use anyhow::anyhow;
5use byteorder::{BigEndian, WriteBytesExt};
6use oasis_core_runtime::consensus::beacon;
7use rand_core::{OsRng, RngCore};
8
9use crate::{
10    context::Context,
11    core::common::crypto::{mrae::deoxysii, x25519},
12    crypto::signature::context::get_chain_context_for,
13    keymanager, module,
14    modules::core::Error,
15    state::CurrentState,
16    types::{
17        self,
18        transaction::{Call, CallFormat, CallResult},
19    },
20};
21
22/// Maximum age of an ephemeral key in the number of epochs.
23///
24/// This is half the current window as enforced by the key manager as negative results are not
25/// cached and randomized queries could open the scheme to a potential DoS attack.
26const MAX_EPHEMERAL_KEY_AGE: beacon::EpochTime = 5;
27
28/// Additional metadata required by the result encoding function.
29pub enum Metadata {
30    Empty,
31    EncryptedX25519DeoxysII {
32        /// Caller's ephemeral public key used for X25519.
33        pk: x25519::PublicKey,
34        /// Secret key.
35        sk: x25519::PrivateKey,
36        /// Transaction index within the batch.
37        index: usize,
38    },
39}
40
41impl std::fmt::Debug for Metadata {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::Empty => f.debug_struct("Metadata::Empty").finish(),
45            Self::EncryptedX25519DeoxysII { pk, index, .. } => f
46                .debug_struct("Metadata::EncryptedX25519DeoxysII")
47                .field("pk", pk)
48                .field("index", index)
49                .finish_non_exhaustive(),
50        }
51    }
52}
53
54/// Derive the key pair ID for the call data encryption key pair.
55pub fn get_key_pair_id(epoch: beacon::EpochTime) -> keymanager::KeyPairId {
56    keymanager::get_key_pair_id([
57        get_chain_context_for(types::callformat::CALL_DATA_KEY_PAIR_ID_CONTEXT_BASE).as_slice(),
58        &epoch.to_be_bytes(),
59    ])
60}
61
62fn verify_epoch<C: Context>(ctx: &C, epoch: beacon::EpochTime) -> Result<(), Error> {
63    if epoch > ctx.epoch() {
64        return Err(Error::InvalidCallFormat(anyhow!("epoch in the future")));
65    }
66    if epoch < ctx.epoch().saturating_sub(MAX_EPHEMERAL_KEY_AGE) {
67        return Err(Error::InvalidCallFormat(anyhow!(
68            "epoch too far in the past"
69        )));
70    }
71    Ok(())
72}
73
74/// Decode call arguments.
75///
76/// Returns `Some((Call, Metadata))` when processing should proceed and `None` in case further
77/// execution needs to be deferred (e.g., because key manager access is required).
78pub fn decode_call<C: Context>(
79    ctx: &C,
80    call: Call,
81    index: usize,
82) -> Result<Option<(Call, Metadata)>, Error> {
83    decode_call_ex(ctx, call, index, false /* assume_km_reachable */)
84}
85
86/// Decode call arguments.
87///
88/// Returns `Some((Call, Metadata))` when processing should proceed and `None` in case further
89/// execution needs to be deferred (e.g., because key manager access is required).
90/// If `assume_km_reachable` is set, then this method will return errors instead of `None`.
91pub fn decode_call_ex<C: Context>(
92    ctx: &C,
93    call: Call,
94    index: usize,
95    assume_km_reachable: bool,
96) -> Result<Option<(Call, Metadata)>, Error> {
97    match call.format {
98        // In case of the plain-text data format, we simply pass on the call unchanged.
99        CallFormat::Plain => Ok(Some((call, Metadata::Empty))),
100
101        // Encrypted data format using X25519 key exchange and Deoxys-II symmetric encryption.
102        CallFormat::EncryptedX25519DeoxysII => {
103            // Method must be empty.
104            if !call.method.is_empty() {
105                return Err(Error::InvalidCallFormat(anyhow!("non-empty method")));
106            }
107            // Body needs to follow the specified envelope.
108            let envelope: types::callformat::CallEnvelopeX25519DeoxysII =
109                cbor::from_value(call.body)
110                    .map_err(|_| Error::InvalidCallFormat(anyhow!("bad call envelope")))?;
111            let pk = envelope.pk;
112
113            // Make sure a key manager is available in this runtime.
114            let key_manager = ctx
115                .key_manager()
116                .ok_or_else(|| Error::InvalidCallFormat(anyhow!("confidential txs unavailable")))?;
117
118            // If we are only doing checks, this is the most that we can do as in this case we may
119            // be unable to access the key manager.
120            if !assume_km_reachable && CurrentState::with_env(|env| !env.is_execute()) {
121                return Ok(None);
122            }
123
124            let decrypt = |epoch: beacon::EpochTime| {
125                let keypair = key_manager
126                    .get_or_create_ephemeral_keys(get_key_pair_id(epoch), epoch)
127                    .map_err(|err| match err {
128                        keymanager::KeyManagerError::InvalidEpoch(..) => {
129                            Error::InvalidCallFormat(anyhow!("invalid epoch"))
130                        }
131                        _ => Error::Abort(err.into()),
132                    })?;
133                let sk = keypair.input_keypair.sk;
134                // Derive shared secret via X25519 and open the sealed box.
135                deoxysii::box_open(
136                    &envelope.nonce,
137                    envelope.data.clone(),
138                    vec![],
139                    &envelope.pk.0,
140                    &sk.0,
141                )
142                .map(|data| (data, sk))
143            };
144
145            // Get transaction key pair from the key manager. Note that only the `input_keypair`
146            // portion is used.
147            let (data, sk) = if envelope.epoch > 0 {
148                verify_epoch(ctx, envelope.epoch)?;
149                decrypt(envelope.epoch)
150            } else {
151                // In case of failure, also try with previous epoch key in case the epoch
152                // transition just occurred.
153                decrypt(ctx.epoch()).or_else(|_| decrypt(ctx.epoch() - 1))
154            }
155            .map_err(Error::InvalidCallFormat)?;
156
157            let read_only = call.read_only;
158            let call: Call = cbor::from_slice(&data)
159                .map_err(|_| Error::InvalidCallFormat(anyhow!("malformed call")))?;
160
161            // Ensure read-only flag is the same as in the outer envelope. This is to prevent
162            // bypassing any authorization based on the read-only flag.
163            if call.read_only != read_only {
164                return Err(Error::InvalidCallFormat(anyhow!("read-only flag mismatch")));
165            }
166
167            Ok(Some((
168                call,
169                Metadata::EncryptedX25519DeoxysII { pk, sk, index },
170            )))
171        }
172    }
173}
174
175#[cfg(any(test, feature = "test"))]
176/// Encodes a call such that it can be decoded by `decode_call[_ex]`.
177pub fn encode_call<C: Context>(
178    ctx: &C,
179    mut call: Call,
180    client_keypair: &(x25519_dalek::PublicKey, x25519_dalek::StaticSecret),
181) -> Result<Call, Error> {
182    match call.format {
183        // In case of the plain-text data format, we simply pass on the call unchanged.
184        CallFormat::Plain => Ok(call),
185
186        // Encrypted data format using X25519 key exchange and Deoxys-II symmetric encryption.
187        CallFormat::EncryptedX25519DeoxysII => {
188            let key_manager = ctx.key_manager().ok_or_else(|| {
189                Error::InvalidCallFormat(anyhow!("confidential transactions not available"))
190            })?;
191            let epoch = ctx.epoch();
192            let runtime_keypair = key_manager
193                .get_or_create_ephemeral_keys(get_key_pair_id(epoch), epoch)
194                .map_err(|err| Error::Abort(err.into()))?;
195            let runtime_pk = runtime_keypair.input_keypair.pk;
196            let nonce = [0u8; deoxysii::NONCE_SIZE];
197
198            Ok(Call {
199                format: call.format,
200                method: std::mem::take(&mut call.method),
201                body: cbor::to_value(types::callformat::CallEnvelopeX25519DeoxysII {
202                    pk: client_keypair.0.into(),
203                    nonce,
204                    epoch,
205                    data: deoxysii::box_seal(
206                        &nonce,
207                        cbor::to_vec(call),
208                        vec![],
209                        &runtime_pk.0,
210                        &client_keypair.1,
211                    )
212                    .unwrap(),
213                }),
214                ..Default::default()
215            })
216        }
217    }
218}
219
220/// Encode call results.
221pub fn encode_result<C: Context>(
222    ctx: &C,
223    result: module::CallResult,
224    metadata: Metadata,
225) -> CallResult {
226    encode_result_ex(ctx, result, metadata, false /* expose_failure */)
227}
228
229/// Encode call results.
230///
231/// If `expose_failure` is set, then this method will not encrypt errors.
232pub fn encode_result_ex<C: Context>(
233    ctx: &C,
234    result: module::CallResult,
235    metadata: Metadata,
236    expose_failure: bool,
237) -> CallResult {
238    match metadata {
239        // In case of the plain-text data format, we simply pass on the data unchanged.
240        Metadata::Empty => result.into(),
241
242        // Encrypted data format using X25519 key exchange and Deoxys-II symmetric encryption.
243        Metadata::EncryptedX25519DeoxysII { pk, sk, index } => {
244            // Serialize result.
245            let result: CallResult = result.into();
246
247            if expose_failure {
248                if result.is_success() {
249                    return CallResult::Ok(encrypt_result_x25519_deoxysii(
250                        ctx, result, pk, sk, index,
251                    ));
252                }
253
254                return result;
255            }
256
257            CallResult::Unknown(encrypt_result_x25519_deoxysii(ctx, result, pk, sk, index))
258        }
259    }
260}
261
262/// Encrypt a call result using the X25519-Deoxys-II encryption scheme.
263pub fn encrypt_result_x25519_deoxysii<C: Context>(
264    ctx: &C,
265    result: types::transaction::CallResult,
266    pk: x25519::PublicKey,
267    sk: x25519::PrivateKey,
268    index: usize,
269) -> cbor::Value {
270    let mut nonce = Vec::with_capacity(deoxysii::NONCE_SIZE);
271    if CurrentState::with_env(|env| env.is_execute()) {
272        // In execution mode generate nonce for the output as Round (8 bytes) || Index (4 bytes) || 00 00 00.
273        nonce
274            .write_u64::<BigEndian>(ctx.runtime_header().round)
275            .unwrap();
276        nonce
277            .write_u32::<BigEndian>(index.try_into().unwrap())
278            .unwrap();
279        nonce.extend(&[0, 0, 0]);
280    } else {
281        // In non-execution mode randomize the nonce to facilitate private queries.
282        nonce.resize(deoxysii::NONCE_SIZE, 0);
283        OsRng.fill_bytes(&mut nonce);
284    }
285    let nonce = nonce.try_into().unwrap();
286    let result = cbor::to_vec(result);
287    let data = deoxysii::box_seal(&nonce, result, vec![], &pk.0, &sk.0).unwrap();
288
289    // Return an envelope.
290    cbor::to_value(types::callformat::ResultEnvelopeX25519DeoxysII { nonce, data })
291}
292
293#[cfg(any(test, feature = "test"))]
294pub fn decode_result<C: Context>(
295    ctx: &C,
296    format: CallFormat,
297    result: CallResult,
298    client_keypair: &(x25519_dalek::PublicKey, x25519_dalek::StaticSecret),
299) -> Result<module::CallResult, Error> {
300    if matches!(format, CallFormat::Plain) {
301        return Ok(result.into_call_result().expect("CallResult was Unknown"));
302    }
303    let envelope_value = match result {
304        CallResult::Ok(v) | CallResult::Unknown(v) => v,
305        CallResult::Failed {
306            module,
307            code,
308            message,
309        } => {
310            return Ok(module::CallResult::Failed {
311                module,
312                code,
313                message,
314            })
315        }
316    };
317    match format {
318        CallFormat::Plain => unreachable!("checked above"),
319        CallFormat::EncryptedX25519DeoxysII => {
320            let envelope: types::callformat::ResultEnvelopeX25519DeoxysII =
321                cbor::from_value(envelope_value)
322                    .map_err(|_| Error::InvalidCallFormat(anyhow!("bad result envelope")))?;
323
324            // Get the runtime pubkey from the KM. A real client would simply use the
325            // session key that has already been derived.
326            let key_manager = ctx
327                .key_manager()
328                .ok_or_else(|| Error::InvalidCallFormat(anyhow!("confidential txs unavailable")))?;
329            let keypair = key_manager
330                .get_or_create_ephemeral_keys(get_key_pair_id(ctx.epoch()), ctx.epoch())
331                .map_err(|err| Error::Abort(err.into()))?;
332            let runtime_pk = keypair.input_keypair.pk;
333
334            let data = deoxysii::box_open(
335                &envelope.nonce,
336                envelope.data,
337                vec![],
338                &runtime_pk.0,
339                &client_keypair.1,
340            )
341            .map_err(Error::InvalidCallFormat)?;
342            let call_result: CallResult = cbor::from_slice(&data)
343                .map_err(|_| Error::InvalidCallFormat(anyhow!("malformed call")))?;
344            Ok(call_result
345                .into_call_result()
346                .expect("CallResult was Unknown"))
347        }
348    }
349}