Skip to content

Commit 0629798

Browse files
authored
Merge pull request oasisprotocol#2148 from oasisprotocol/kostko/feature/rofl-kd-scopes
runtime-sdk/modules/rofl: Add key scope in rofl.DeriveKey
2 parents e9629ef + 4e6ced9 commit 0629798

File tree

12 files changed

+1851
-1264
lines changed

12 files changed

+1851
-1264
lines changed

Cargo.lock

Lines changed: 287 additions & 257 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contract-sdk/specs/access/oas173/Cargo.lock

Lines changed: 158 additions & 149 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contract-sdk/specs/token/oas20/Cargo.lock

Lines changed: 158 additions & 149 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/runtime-sdk/rofl-oracle-tdx/Cargo.lock

Lines changed: 579 additions & 291 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/runtime-sdk/rofl-oracle/Cargo.lock

Lines changed: 255 additions & 246 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime-sdk/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "oasis-runtime-sdk"
3-
version = "0.10.0"
3+
version = "0.10.1"
44
authors = ["Oasis Protocol Foundation <info@oasisprotocol.org>"]
55
edition = "2021"
66
license = "Apache-2.0"

runtime-sdk/src/modules/rofl/app/client.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ impl Default for SubmitTxOpts {
7171
pub struct DeriveKeyRequest {
7272
/// Key kind.
7373
pub kind: modules::rofl::types::KeyKind,
74+
/// Key scope.
75+
pub scope: modules::rofl::types::KeyScope,
7476
/// Key generation.
7577
pub generation: u64,
7678
/// Key identifier.
@@ -198,6 +200,7 @@ where
198200
modules::rofl::types::DeriveKey {
199201
app: A::id(),
200202
kind: request.kind,
203+
scope: request.scope,
201204
generation: request.generation,
202205
key_id: request.key_id,
203206
},

runtime-sdk/src/modules/rofl/mod.rs

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! On-chain coordination for ROFL components.
2-
use std::collections::BTreeSet;
2+
use std::collections::{BTreeMap, BTreeSet};
33

44
use once_cell::sync::Lazy;
55

@@ -217,9 +217,16 @@ impl<Cfg: Config> Module<Cfg> {
217217
)?;
218218

219219
// Generate the secret encryption (public) key.
220-
let sek = Self::derive_app_key(ctx, &app_id, types::KeyKind::X25519, ROFL_KEY_ID_SEK)?
221-
.input_keypair
222-
.pk;
220+
let sek = Self::derive_app_key(
221+
ctx,
222+
&app_id,
223+
types::KeyKind::X25519,
224+
types::KeyScope::Global,
225+
ROFL_KEY_ID_SEK,
226+
None,
227+
)?
228+
.input_keypair
229+
.pk;
223230

224231
// Register the application.
225232
let cfg = types::AppConfig {
@@ -263,9 +270,16 @@ impl<Cfg: Config> Module<Cfg> {
263270

264271
// If there is no SEK defined, regenerate it.
265272
if cfg.sek == Default::default() {
266-
cfg.sek = Self::derive_app_key(ctx, &body.id, types::KeyKind::X25519, ROFL_KEY_ID_SEK)?
267-
.input_keypair
268-
.pk;
273+
cfg.sek = Self::derive_app_key(
274+
ctx,
275+
&body.id,
276+
types::KeyKind::X25519,
277+
types::KeyScope::Global,
278+
ROFL_KEY_ID_SEK,
279+
None,
280+
)?
281+
.input_keypair
282+
.pk;
269283
}
270284

271285
cfg.policy = body.policy;
@@ -403,17 +417,27 @@ impl<Cfg: Config> Module<Cfg> {
403417
return Err(Error::InvalidArgument);
404418
}
405419

420+
// Disallow invocation from subcalls.
421+
if CurrentState::with_env(|env| env.is_internal()) {
422+
return Err(Error::Forbidden);
423+
}
424+
406425
if CurrentState::with_env(|env| env.is_check_only()) {
407426
return Ok(Default::default());
408427
}
409428

410429
// Ensure caller is an authorized instance of the given application.
411-
if !Self::is_authorized_origin(body.app) {
412-
return Err(Error::Forbidden);
413-
}
430+
let reg = Self::get_origin_registration(body.app).ok_or(Error::Forbidden)?;
414431

415432
// Derive application key.
416-
let key = Self::derive_app_key(ctx, &body.app, body.kind, &body.key_id)?;
433+
let key = Self::derive_app_key(
434+
ctx,
435+
&body.app,
436+
body.kind,
437+
body.scope,
438+
&body.key_id,
439+
Some(reg),
440+
)?;
417441
let key = match body.kind {
418442
types::KeyKind::EntropyV0 => key.state_key.0.into(),
419443
types::KeyKind::X25519 => key.input_keypair.sk.as_ref().into(),
@@ -422,18 +446,75 @@ impl<Cfg: Config> Module<Cfg> {
422446
Ok(types::DeriveKeyResponse { key })
423447
}
424448

449+
fn derive_app_key_id(
450+
app: &app_id::AppId,
451+
kind: types::KeyKind,
452+
scope: types::KeyScope,
453+
key_id: &[u8],
454+
reg: Option<types::Registration>,
455+
) -> Result<keymanager::KeyPairId, Error> {
456+
// Build the base key identifier.
457+
//
458+
// We use the following tuple elements which are fed into TupleHash to derive the final key
459+
// identifier, in order:
460+
//
461+
// - V1 context domain separator.
462+
// - App ID.
463+
// - Encoded kind.
464+
// - Key ID.
465+
// - Optional CBOR-serialized extra domain separation.
466+
//
467+
let kind_id = &[kind as u8];
468+
let mut key_id = vec![ROFL_DERIVE_KEY_CONTEXT, app.as_ref(), kind_id, key_id];
469+
let mut extra_dom: BTreeMap<&str, Vec<u8>> = BTreeMap::new();
470+
471+
match scope {
472+
types::KeyScope::Global => {
473+
// Nothing to do here, global keys don't include an explicit scope for backwards
474+
// compatibility.
475+
}
476+
types::KeyScope::Node => {
477+
// Fetch node identifier corresponding to the application instance.
478+
let node_id = reg.ok_or(Error::InvalidArgument)?.node_id;
479+
480+
extra_dom.insert("scope", [scope as u8].to_vec());
481+
extra_dom.insert("node_id", node_id.as_ref().to_vec());
482+
}
483+
types::KeyScope::Entity => {
484+
// Fetch entity identifier corresponding to the application instance.
485+
let entity_id = reg
486+
.ok_or(Error::InvalidArgument)?
487+
.entity_id
488+
.ok_or(Error::InvalidArgument)?;
489+
490+
extra_dom.insert("scope", [scope as u8].to_vec());
491+
extra_dom.insert("entity_id", entity_id.as_ref().to_vec());
492+
}
493+
};
494+
495+
// Add optional extra domain separation.
496+
let extra_dom = if !extra_dom.is_empty() {
497+
cbor::to_vec(extra_dom)
498+
} else {
499+
vec![]
500+
};
501+
if !extra_dom.is_empty() {
502+
key_id.push(&extra_dom)
503+
}
504+
505+
// Finalize the key identifier.
506+
Ok(keymanager::get_key_pair_id(key_id))
507+
}
508+
425509
fn derive_app_key<C: Context>(
426510
ctx: &C,
427511
app: &app_id::AppId,
428512
kind: types::KeyKind,
513+
scope: types::KeyScope,
429514
key_id: &[u8],
515+
reg: Option<types::Registration>,
430516
) -> Result<keymanager::KeyPair, Error> {
431-
let key_id = keymanager::get_key_pair_id([
432-
ROFL_DERIVE_KEY_CONTEXT,
433-
app.as_ref(),
434-
&[kind as u8],
435-
key_id,
436-
]);
517+
let key_id = Self::derive_app_key_id(app, kind, scope, key_id, reg)?;
437518

438519
let km = ctx
439520
.key_manager()

runtime-sdk/src/modules/rofl/test.rs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,70 @@ fn test_create_scheme() {
242242
);
243243
}
244244

245+
#[test]
246+
fn test_derive_app_key_id() {
247+
// Ensure that app key identifier derivation is stable.
248+
let tcs = [
249+
(
250+
"rofl1qqfuf7u556prwv0wkdt398prhrpat7r3rvr97khf",
251+
types::KeyKind::EntropyV0,
252+
types::KeyScope::Global,
253+
b"test key",
254+
None,
255+
"d5c2af4a445f893f3cc395fd7ed3aca53ecc98f4ff93401f03d8e17b9dac563f",
256+
),
257+
(
258+
"rofl1qqfuf7u556prwv0wkdt398prhrpat7r3rvr97khf",
259+
types::KeyKind::X25519,
260+
types::KeyScope::Global,
261+
b"test key",
262+
None,
263+
"e1ad6dfca49ca3449642a8334c97120bbdeaf778e2c5b44f06b3584d9d77edbb",
264+
),
265+
(
266+
"rofl1qqfuf7u556prwv0wkdt398prhrpat7r3rvr97khf",
267+
types::KeyKind::EntropyV0,
268+
types::KeyScope::Node,
269+
b"test key",
270+
Some(types::Registration {
271+
node_id: "0000000000000000000000000000000000000000000000000000000000000000".into(),
272+
..Default::default()
273+
}),
274+
"90725fe1f83e647f2a6ae917e076b5c42cf64944efa79fa4707a39583ff89b26",
275+
),
276+
(
277+
"rofl1qqfuf7u556prwv0wkdt398prhrpat7r3rvr97khf",
278+
types::KeyKind::EntropyV0,
279+
types::KeyScope::Node,
280+
b"test key",
281+
Some(types::Registration {
282+
node_id: "1111111111111111111111111111111111111111111111111111111111111111".into(),
283+
..Default::default()
284+
}),
285+
"06374f036728adfbd64a2bb10d55a3d8ea8de122305383ea27064e12caafba71",
286+
),
287+
(
288+
"rofl1qqfuf7u556prwv0wkdt398prhrpat7r3rvr97khf",
289+
types::KeyKind::EntropyV0,
290+
types::KeyScope::Entity,
291+
b"test key",
292+
Some(types::Registration {
293+
entity_id: Some(
294+
"1111111111111111111111111111111111111111111111111111111111111111".into(),
295+
),
296+
..Default::default()
297+
}),
298+
"5e3ec31fb6648b48fa8e721796fb1d01d11fee50303fd382118ddf9e69d6f77b",
299+
),
300+
];
301+
for tc in tcs {
302+
let key_id =
303+
Module::<Config>::derive_app_key_id(&tc.0.into(), tc.1, tc.2, tc.3, tc.4.clone())
304+
.unwrap();
305+
assert_eq!(key_id, tc.5.into(), "{:?}", tc);
306+
}
307+
}
308+
245309
#[test]
246310
fn test_key_derivation() {
247311
let mut mock = mock::Mock::default();
@@ -262,6 +326,7 @@ fn test_key_derivation() {
262326
let derive = types::DeriveKey {
263327
app,
264328
kind: types::KeyKind::EntropyV0,
329+
scope: types::KeyScope::Global,
265330
generation: 0,
266331
key_id: b"my test key".into(),
267332
};
@@ -292,7 +357,7 @@ fn test_key_derivation() {
292357
extra_keys: vec![keys::alice::pk()],
293358
..Default::default()
294359
};
295-
state::update_registration(fake_registration).unwrap();
360+
state::update_registration(fake_registration.clone()).unwrap();
296361

297362
// The call should succeed now.
298363
let dispatch_result = signer_alice.call_opts(
@@ -343,4 +408,62 @@ fn test_key_derivation() {
343408
let (err_module, err_code) = dispatch_result.result.unwrap_failed();
344409
assert_eq!(&err_module, "rofl");
345410
assert_eq!(err_code, 1);
411+
412+
// Try different scopes.
413+
let dispatch_result = signer_alice.call_opts(
414+
&ctx,
415+
"rofl.DeriveKey",
416+
types::DeriveKey {
417+
scope: types::KeyScope::Node,
418+
..derive.clone()
419+
},
420+
CallOptions {
421+
encrypted: true,
422+
..Default::default()
423+
},
424+
);
425+
let dispatch_result = dispatch_result.result.unwrap();
426+
let result: types::DeriveKeyResponse = cbor::from_value(dispatch_result).unwrap();
427+
assert!(!result.key.is_empty());
428+
429+
// Entity scope should fail as the registration doesn't have an entity set.
430+
let dispatch_result = signer_alice.call_opts(
431+
&ctx,
432+
"rofl.DeriveKey",
433+
types::DeriveKey {
434+
scope: types::KeyScope::Entity,
435+
..derive.clone()
436+
},
437+
CallOptions {
438+
encrypted: true,
439+
..Default::default()
440+
},
441+
);
442+
let (err_module, err_code) = dispatch_result.result.unwrap_failed();
443+
assert_eq!(&err_module, "rofl");
444+
assert_eq!(err_code, 1);
445+
446+
// Update registration to include an entity.
447+
state::update_registration(types::Registration {
448+
entity_id: Some(Default::default()),
449+
..fake_registration.clone()
450+
})
451+
.unwrap();
452+
453+
// Entity scope should now work.
454+
let dispatch_result = signer_alice.call_opts(
455+
&ctx,
456+
"rofl.DeriveKey",
457+
types::DeriveKey {
458+
scope: types::KeyScope::Entity,
459+
..derive.clone()
460+
},
461+
CallOptions {
462+
encrypted: true,
463+
..Default::default()
464+
},
465+
);
466+
let dispatch_result = dispatch_result.result.unwrap();
467+
let result: types::DeriveKeyResponse = cbor::from_value(dispatch_result).unwrap();
468+
assert!(!result.key.is_empty());
346469
}

runtime-sdk/src/modules/rofl/types.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,39 @@ pub enum KeyKind {
117117
X25519 = 1,
118118
}
119119

120+
/// Scope of key for derivation.
121+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
122+
#[cbor(with_default)]
123+
#[repr(u8)]
124+
pub enum KeyScope {
125+
/// Global application scope (e.g. all instances get the same key).
126+
#[default]
127+
Global = 0,
128+
129+
/// Node scope (e.g. all instances endorsed by the same node get the same key).
130+
Node = 1,
131+
132+
/// Entity scope (e.g. all instances endorsed by nodes from the same entity get the same key).
133+
Entity = 2,
134+
}
135+
136+
impl KeyScope {
137+
/// Whether this key scope is the global key scope.
138+
pub fn is_global(&self) -> bool {
139+
matches!(self, Self::Global)
140+
}
141+
}
142+
120143
/// Derive key call.
121144
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
122145
pub struct DeriveKey {
123146
/// ROFL application identifier.
124147
pub app: AppId,
125148
/// Key kind.
126149
pub kind: KeyKind,
150+
/// Key scope.
151+
#[cbor(optional, skip_serializing_if = "KeyScope::is_global")]
152+
pub scope: KeyScope,
127153
/// Key generation.
128154
pub generation: u64,
129155
/// Key identifier.

0 commit comments

Comments
 (0)