diff --git a/client-sdk/go/modules/rofl/rofl.go b/client-sdk/go/modules/rofl/rofl.go index e0a7ae2328..020ebc0b02 100644 --- a/client-sdk/go/modules/rofl/rofl.go +++ b/client-sdk/go/modules/rofl/rofl.go @@ -19,6 +19,7 @@ var ( // Queries. methodApp = types.NewMethodName("rofl.App", AppQuery{}) + methodAppInstance = types.NewMethodName("rofl.AppInstance", AppInstanceQuery{}) methodAppInstances = types.NewMethodName("rofl.AppInstances", AppQuery{}) methodParameters = types.NewMethodName("rofl.Parameters", nil) ) @@ -39,6 +40,9 @@ type V1 interface { // App queries the given application configuration. App(ctx context.Context, round uint64, id AppID) (*AppConfig, error) + // AppInstance queries a specific registered instance of the given application. + AppInstance(ctx context.Context, round uint64, id AppID, rak types.PublicKey) (*Registration, error) + // AppInstances queries the registered instances of the given application. AppInstances(ctx context.Context, round uint64, id AppID) ([]*Registration, error) @@ -86,6 +90,16 @@ func (a *v1) App(ctx context.Context, round uint64, id AppID) (*AppConfig, error return &appCfg, nil } +// Implements V1. +func (a *v1) AppInstance(ctx context.Context, round uint64, id AppID, rak types.PublicKey) (*Registration, error) { + var instance Registration + err := a.rc.Query(ctx, round, methodAppInstance, AppInstanceQuery{App: id, RAK: rak}, &instance) + if err != nil { + return nil, err + } + return &instance, nil +} + // Implements V1. func (a *v1) AppInstances(ctx context.Context, round uint64, id AppID) ([]*Registration, error) { var instances []*Registration diff --git a/client-sdk/go/modules/rofl/types.go b/client-sdk/go/modules/rofl/types.go index f433527a12..1e38bc8069 100644 --- a/client-sdk/go/modules/rofl/types.go +++ b/client-sdk/go/modules/rofl/types.go @@ -62,6 +62,14 @@ type AppQuery struct { ID AppID `json:"id"` } +// AppInstanceQuery is an application instance query. +type AppInstanceQuery struct { + // App is the application identifier. + App AppID `json:"app"` + // RAK is the Runtime Attestation Key. + RAK types.PublicKey `json:"rak"` +} + // AppConfig is a ROFL application configuration. type AppConfig struct { // ID is the application identifier. @@ -80,6 +88,8 @@ type Registration struct { App AppID `json:"app"` // NodeID is the identifier of the endorsing node. NodeID signature.PublicKey `json:"node_id"` + // EntityID is the optional identifier of the endorsing entity. + EntityID *signature.PublicKey `json:"entity_id,omitempty"` // RAK is the Runtime Attestation Key. RAK signature.PublicKey `json:"rak"` // REK is the Runtime Encryption Key. diff --git a/runtime-sdk/src/modules/rofl/config.rs b/runtime-sdk/src/modules/rofl/config.rs index 939fd3c652..7a44a69a01 100644 --- a/runtime-sdk/src/modules/rofl/config.rs +++ b/runtime-sdk/src/modules/rofl/config.rs @@ -12,6 +12,10 @@ pub trait Config: 'static { const GAS_COST_CALL_REGISTER: u64 = 100_000; /// Gas cost of rofl.IsAuthorizedOrigin call. const GAS_COST_CALL_IS_AUTHORIZED_ORIGIN: u64 = 1000; + /// Gas cost of rofl.AuthorizedOriginNode call. + const GAS_COST_CALL_AUTHORIZED_ORIGIN_NODE: u64 = 2000; + /// Gas cost of rofl.AuthorizedOriginEntity call. + const GAS_COST_CALL_AUTHORIZED_ORIGIN_ENTITY: u64 = 2000; /// Amount of stake required for maintaining an application. /// diff --git a/runtime-sdk/src/modules/rofl/error.rs b/runtime-sdk/src/modules/rofl/error.rs index 5c96f52888..d27d3eb69a 100644 --- a/runtime-sdk/src/modules/rofl/error.rs +++ b/runtime-sdk/src/modules/rofl/error.rs @@ -49,6 +49,10 @@ pub enum Error { #[sdk_error(code = 11)] Forbidden, + #[error("unknown instance")] + #[sdk_error(code = 12)] + UnknownInstance, + #[error("core: {0}")] #[sdk_error(transparent)] Core(#[from] modules::core::Error), diff --git a/runtime-sdk/src/modules/rofl/mod.rs b/runtime-sdk/src/modules/rofl/mod.rs index d8382a7985..90d4bc2434 100644 --- a/runtime-sdk/src/modules/rofl/mod.rs +++ b/runtime-sdk/src/modules/rofl/mod.rs @@ -5,9 +5,12 @@ use once_cell::sync::Lazy; use crate::{ context::Context, - core::consensus::{ - registry::{Node, RolesMask, VerifiedEndorsedCapabilityTEE}, - state::registry::ImmutableState as RegistryImmutableState, + core::{ + common::crypto::signature::PublicKey as CorePublicKey, + consensus::{ + registry::{Node, RolesMask, VerifiedEndorsedCapabilityTEE}, + state::registry::ImmutableState as RegistryImmutableState, + }, }, crypto::signature::PublicKey, handler, migration, @@ -64,13 +67,32 @@ pub struct Genesis { /// Interface that can be called from other modules. pub trait API { + /// Get the Runtime Attestation Key of the ROFL app instance in case the origin transaction is + /// signed by a ROFL instance. Otherwise `None` is returned. + /// + /// # Panics + /// + /// This method will panic if called outside a transaction environment. + fn get_origin_rak() -> Option; + + /// Get the registration descriptor of the ROFL app instance in case the origin transaction is + /// signed by a ROFL instance of the specified app. Otherwise `None` is returned. + /// + /// # Panics + /// + /// This method will panic if called outside a transaction environment. + fn get_origin_registration(app: app_id::AppId) -> Option; + /// Verify whether the origin transaction is signed by an authorized ROFL instance for the given /// application. /// /// # Panics /// /// This method will panic if called outside a transaction environment. - fn is_authorized_origin(app: app_id::AppId) -> Result; + fn is_authorized_origin(app: app_id::AppId) -> bool; + + /// Get a specific registered instance for an application. + fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result; /// Get an application's configuration. fn get_app(id: app_id::AppId) -> Result; @@ -90,22 +112,30 @@ pub struct Module { } impl API for Module { - fn is_authorized_origin(app: app_id::AppId) -> Result { - let caller_pk = CurrentState::with_env_origin(|env| env.tx_caller_public_key()) - .ok_or(Error::InvalidArgument)?; + fn get_origin_rak() -> Option { + let caller_pk = CurrentState::with_env_origin(|env| env.tx_caller_public_key())?; // Resolve RAK as the call may be made by an extra key. - let rak = match state::get_endorser(&caller_pk) { + state::get_endorser(&caller_pk).map(|kei| match kei { // It may point to a RAK. - Some(state::KeyEndorsementInfo { rak: Some(rak), .. }) => rak, + state::KeyEndorsementInfo { rak: Some(rak), .. } => rak.into(), // Or it points to itself. - Some(_) => caller_pk.try_into().map_err(|_| Error::InvalidArgument)?, - // Or is unknown. - None => return Ok(false), - }; + _ => caller_pk, + }) + } + + fn get_origin_registration(app: app_id::AppId) -> Option { + Self::get_origin_rak() + .and_then(|rak| state::get_registration(app, &rak.try_into().unwrap())) + } - // Check whether the the endorsement is for the right application. - Ok(state::get_registration(app, &rak).is_some()) + fn is_authorized_origin(app: app_id::AppId) -> bool { + Self::get_origin_registration(app).is_some() + } + + fn get_registration(app: app_id::AppId, rak: PublicKey) -> Result { + state::get_registration(app, &rak.try_into().map_err(|_| Error::InvalidArgument)?) + .ok_or(Error::UnknownInstance) } fn get_app(id: app_id::AppId) -> Result { @@ -314,12 +344,13 @@ impl Module { } // Verify allowed endorsement. - Self::verify_endorsement(ctx, &cfg.policy, &verified_ect)?; + let node = Self::verify_endorsement(ctx, &cfg.policy, &verified_ect)?; // Update registration. let registration = types::Registration { app: body.app, node_id: verified_ect.node_id.unwrap(), // Verified above. + entity_id: node.map(|n| n.entity_id), rak: body.ect.capability_tee.rak, rek: body.ect.capability_tee.rek.ok_or(Error::InvalidArgument)?, // REK required. expiration: body.expiration, @@ -331,18 +362,20 @@ impl Module { } /// Verify whether the given endorsement is allowed by the application policy. + /// + /// Returns an optional endorsing node descriptor when available. fn verify_endorsement( ctx: &C, app_policy: &policy::AppAuthPolicy, ect: &VerifiedEndorsedCapabilityTEE, - ) -> Result<(), Error> { + ) -> Result, Error> { use policy::AllowedEndorsement; let endorsing_node_id = ect.node_id.ok_or(Error::UnknownNode)?; // Attempt to resolve the node that endorsed the enclave. It may be that the node is not // even registered in the consensus layer which may be acceptable for some policies. - let node = || -> Result, Error> { + let maybe_node = || -> Result, Error> { let registry = RegistryImmutableState::new(ctx.consensus_state()); let node = registry .node(&endorsing_node_id) @@ -367,30 +400,30 @@ impl Module { }; for allowed in &app_policy.endorsements { - match (allowed, &node) { + match (allowed, &maybe_node) { (AllowedEndorsement::Any, _) => { // Any node is allowed. - return Ok(()); + return Ok(maybe_node); } (AllowedEndorsement::ComputeRole, Some(node)) => { if node.has_roles(RolesMask::ROLE_COMPUTE_WORKER) && has_runtime(node) { - return Ok(()); + return Ok(maybe_node); } } (AllowedEndorsement::ObserverRole, Some(node)) => { if node.has_roles(RolesMask::ROLE_OBSERVER) && has_runtime(node) { - return Ok(()); + return Ok(maybe_node); } } (AllowedEndorsement::Entity(entity_id), Some(node)) => { // If a specific entity is required, it may be registered for any runtime. if &node.entity_id == entity_id { - return Ok(()); + return Ok(maybe_node); } } (AllowedEndorsement::Node(node_id), _) => { if endorsing_node_id == *node_id { - return Ok(()); + return Ok(maybe_node); } } _ => continue, @@ -410,7 +443,29 @@ impl Module { ) -> Result { ::Core::use_tx_gas(Cfg::GAS_COST_CALL_IS_AUTHORIZED_ORIGIN)?; - Self::is_authorized_origin(app) + Ok(Self::is_authorized_origin(app)) + } + + #[handler(call = "rofl.AuthorizedOriginNode", internal)] + fn internal_authorized_origin_node( + _ctx: &C, + app: app_id::AppId, + ) -> Result { + ::Core::use_tx_gas(Cfg::GAS_COST_CALL_AUTHORIZED_ORIGIN_NODE)?; + + let registration = Self::get_origin_registration(app).ok_or(Error::UnknownInstance)?; + Ok(registration.node_id) + } + + #[handler(call = "rofl.AuthorizedOriginEntity", internal)] + fn internal_authorized_origin_entity( + _ctx: &C, + app: app_id::AppId, + ) -> Result, Error> { + ::Core::use_tx_gas(Cfg::GAS_COST_CALL_AUTHORIZED_ORIGIN_ENTITY)?; + + let registration = Self::get_origin_registration(app).ok_or(Error::UnknownInstance)?; + Ok(registration.entity_id) } /// Returns the configuration for the given ROFL application. @@ -419,6 +474,15 @@ impl Module { Self::get_app(args.id) } + /// Returns a specific registered instance for the given ROFL application. + #[handler(query = "rofl.AppInstance")] + fn query_app_instance( + _ctx: &C, + args: types::AppInstanceQuery, + ) -> Result { + Self::get_registration(args.app, args.rak) + } + /// Returns a list of all registered instances for the given ROFL application. #[handler(query = "rofl.AppInstances", expensive)] fn query_app_instances( diff --git a/runtime-sdk/src/modules/rofl/types.rs b/runtime-sdk/src/modules/rofl/types.rs index 4a088d36d0..db4cf8dc0b 100644 --- a/runtime-sdk/src/modules/rofl/types.rs +++ b/runtime-sdk/src/modules/rofl/types.rs @@ -81,6 +81,8 @@ pub struct Registration { pub app: AppId, /// Identifier of the endorsing node. pub node_id: signature::PublicKey, + /// Optional identifier of the endorsing entity. + pub entity_id: Option, /// Runtime Attestation Key. pub rak: signature::PublicKey, /// Runtime Encryption Key. @@ -97,3 +99,13 @@ pub struct AppQuery { /// ROFL application identifier. pub id: AppId, } + +/// Application instance query. +#[derive(Clone, Debug, cbor::Encode, cbor::Decode)] +#[cbor(no_default)] +pub struct AppInstanceQuery { + /// ROFL application identifier. + pub app: AppId, + /// Runtime Attestation Key. + pub rak: PublicKey, +} diff --git a/tests/e2e/rofl/tests.go b/tests/e2e/rofl/tests.go index 2f9130c0b1..410b28b179 100644 --- a/tests/e2e/rofl/tests.go +++ b/tests/e2e/rofl/tests.go @@ -3,11 +3,14 @@ package rofl import ( "context" "fmt" + "reflect" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/ed25519" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/accounts" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/testing" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" "github.com/oasisprotocol/oasis-sdk/tests/e2e/scenario" ) @@ -125,9 +128,23 @@ func QueryTest(ctx context.Context, env *scenario.Env) error { env.Logger.Info("retrieved application instance", "app", ins.App, "node_id", ins.NodeID, + "entity_id", ins.EntityID, "rak", ins.RAK, "expiration", ins.Expiration, ) + + rak := types.PublicKey{ + PublicKey: ed25519.PublicKey(ins.RAK), + } + + // Query individual instance and ensure it is equal. + instance, err := rf.AppInstance(ctx, client.RoundLatest, exampleAppID, rak) + if err != nil { + return fmt.Errorf("failed to query instance '%s': %w", rak, err) + } + if !reflect.DeepEqual(ins, instance) { + return fmt.Errorf("instance mismatch") + } } // There should be 3 instances, one for each compute node. diff --git a/tests/runtimes/components-ronl/src/oracle/mod.rs b/tests/runtimes/components-ronl/src/oracle/mod.rs index d5f97e247d..8f41de5dca 100644 --- a/tests/runtimes/components-ronl/src/oracle/mod.rs +++ b/tests/runtimes/components-ronl/src/oracle/mod.rs @@ -102,7 +102,7 @@ impl Module { ::Core::use_tx_gas(Cfg::GAS_COST_CALL_OBSERVE)?; // Ensure that the observation was processed by the configured ROFL application. - if !Cfg::Rofl::is_authorized_origin(Cfg::rofl_app_id())? { + if !Cfg::Rofl::is_authorized_origin(Cfg::rofl_app_id()) { return Err(Error::NotAuthorized); }