Skip to content

Commit

Permalink
Merge pull request #1954 from oasisprotocol/kostko/feature/rofl-origi…
Browse files Browse the repository at this point in the history
…n-node-ent

runtime-sdk/modules/rofl: Add rofl.AuthorizedOrigin{Node,Entity}
  • Loading branch information
kostko authored Aug 28, 2024
2 parents 68e084b + 7e1da56 commit 58ae251
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 26 deletions.
14 changes: 14 additions & 0 deletions client-sdk/go/modules/rofl/rofl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions client-sdk/go/modules/rofl/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions runtime-sdk/src/modules/rofl/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
4 changes: 4 additions & 0 deletions runtime-sdk/src/modules/rofl/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
114 changes: 89 additions & 25 deletions runtime-sdk/src/modules/rofl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<PublicKey>;

/// 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<types::Registration>;

/// 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<bool, Error>;
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<types::Registration, Error>;

/// Get an application's configuration.
fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error>;
Expand All @@ -90,22 +112,30 @@ pub struct Module<Cfg: Config> {
}

impl<Cfg: Config> API for Module<Cfg> {
fn is_authorized_origin(app: app_id::AppId) -> Result<bool, Error> {
let caller_pk = CurrentState::with_env_origin(|env| env.tx_caller_public_key())
.ok_or(Error::InvalidArgument)?;
fn get_origin_rak() -> Option<PublicKey> {
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<types::Registration> {
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<types::Registration, Error> {
state::get_registration(app, &rak.try_into().map_err(|_| Error::InvalidArgument)?)
.ok_or(Error::UnknownInstance)
}

fn get_app(id: app_id::AppId) -> Result<types::AppConfig, Error> {
Expand Down Expand Up @@ -314,12 +344,13 @@ impl<Cfg: Config> Module<Cfg> {
}

// 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,
Expand All @@ -331,18 +362,20 @@ impl<Cfg: Config> Module<Cfg> {
}

/// Verify whether the given endorsement is allowed by the application policy.
///
/// Returns an optional endorsing node descriptor when available.
fn verify_endorsement<C: Context>(
ctx: &C,
app_policy: &policy::AppAuthPolicy,
ect: &VerifiedEndorsedCapabilityTEE,
) -> Result<(), Error> {
) -> Result<Option<Node>, 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<Option<Node>, Error> {
let maybe_node = || -> Result<Option<Node>, Error> {
let registry = RegistryImmutableState::new(ctx.consensus_state());
let node = registry
.node(&endorsing_node_id)
Expand All @@ -367,30 +400,30 @@ impl<Cfg: Config> Module<Cfg> {
};

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,
Expand All @@ -410,7 +443,29 @@ impl<Cfg: Config> Module<Cfg> {
) -> Result<bool, Error> {
<C::Runtime as Runtime>::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<C: Context>(
_ctx: &C,
app: app_id::AppId,
) -> Result<CorePublicKey, Error> {
<C::Runtime as Runtime>::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<C: Context>(
_ctx: &C,
app: app_id::AppId,
) -> Result<Option<CorePublicKey>, Error> {
<C::Runtime as Runtime>::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.
Expand All @@ -419,6 +474,15 @@ impl<Cfg: Config> Module<Cfg> {
Self::get_app(args.id)
}

/// Returns a specific registered instance for the given ROFL application.
#[handler(query = "rofl.AppInstance")]
fn query_app_instance<C: Context>(
_ctx: &C,
args: types::AppInstanceQuery,
) -> Result<types::Registration, Error> {
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<C: Context>(
Expand Down
12 changes: 12 additions & 0 deletions runtime-sdk/src/modules/rofl/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<signature::PublicKey>,
/// Runtime Attestation Key.
pub rak: signature::PublicKey,
/// Runtime Encryption Key.
Expand All @@ -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,
}
17 changes: 17 additions & 0 deletions tests/e2e/rofl/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/runtimes/components-ronl/src/oracle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl<Cfg: Config> Module<Cfg> {
<C::Runtime as Runtime>::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);
}

Expand Down

0 comments on commit 58ae251

Please sign in to comment.