diff --git a/.changelog/6387.breaking.md b/.changelog/6387.breaking.md new file mode 100644 index 00000000000..b1d440b6547 --- /dev/null +++ b/.changelog/6387.breaking.md @@ -0,0 +1,4 @@ +Support per-role quote policies + +This enables a more relaxed general policy that all attestations must satisy +but a stricter policies for the nodes that can access the key manager. diff --git a/go/common/node/node.go b/go/common/node/node.go index 27bd22bbd8b..5303891627a 100644 --- a/go/common/node/node.go +++ b/go/common/node/node.go @@ -574,7 +574,7 @@ func HashRAK(rak signature.PublicKey) hash.Hash { } // Verify verifies the node's TEE capabilities, at the provided timestamp and height. -func (c *CapabilityTEE) Verify(teeCfg *TEEFeatures, ts time.Time, height uint64, constraints []byte, nodeID signature.PublicKey, isFeatureVersion242 bool) error { +func (c *CapabilityTEE) Verify(teeCfg *TEEFeatures, ts time.Time, height uint64, constraints []byte, n *Node, isFeatureVersion242 bool) error { switch c.Hardware { case TEEHardwareIntelSGX: // Parse SGX remote attestation. @@ -596,7 +596,7 @@ func (c *CapabilityTEE) Verify(teeCfg *TEEFeatures, ts time.Time, height uint64, } // Verify SGX remote attestation. - return sa.Verify(teeCfg, ts, height, &sc, c.RAK, c.REK, nodeID) + return sa.Verify(teeCfg, ts, height, &sc, c.RAK, c.REK, n) default: return ErrInvalidTEEHardware } diff --git a/go/common/node/sgx.go b/go/common/node/sgx.go index 9b188cbe5d7..14e44046060 100644 --- a/go/common/node/sgx.go +++ b/go/common/node/sgx.go @@ -32,9 +32,12 @@ type SGXConstraints struct { // Enclaves is the allowed MRENCLAVE/MRSIGNER pairs. Enclaves []sgx.EnclaveIdentity `json:"enclaves,omitempty"` - // Policy is the quote policy. + // Policy is the quote policy that all attestations must satisfy. Policy *quote.Policy `json:"policy,omitempty"` + // PerRolePolicy defines additional role specific quote policies. + PerRolePolicy map[RolesMask]*quote.Policy `json:"per_role_policy,omitempty"` + // MaxAttestationAge is the maximum attestation age (in blocks). MaxAttestationAge uint64 `json:"max_attestation_age,omitempty"` } @@ -111,18 +114,40 @@ func (sc *SGXConstraints) ValidateBasic(cfg *TEEFeatures, isFeatureVersion242 bo return fmt.Errorf("unsupported SGX constraints version: %d", sc.V) } - // Check for TDX enablement. - if !cfg.SGX.TDX && sc.Policy.PCS != nil && sc.Policy.PCS.TDX != nil { - return fmt.Errorf("TDX policy not supported") + validatePolicy := func(policy *quote.Policy) error { + // Check for TDX enablement. + if !cfg.SGX.TDX && policy.PCS != nil && policy.PCS.TDX != nil { + return fmt.Errorf("TDX policy not supported") + } + if err := policy.Validate(isFeatureVersion242); err != nil { + return fmt.Errorf("invalid policy: %w", err) + } + return nil } - // Check that policy is compliant with the current feature version. + // Check default policy. if sc.Policy != nil { - if err := sc.Policy.Validate(isFeatureVersion242); err != nil { + if err := validatePolicy(sc.Policy); err != nil { return fmt.Errorf("invalid policy: %w", err) } } + // Check per-role policies. + if !isFeatureVersion242 && sc.PerRolePolicy != nil { + return fmt.Errorf("per role policy should be empty") + } + for role, policy := range sc.PerRolePolicy { + if !role.IsSingleRole() { + return fmt.Errorf("per-role quote policies should have a single role") + } + if policy == nil { + return fmt.Errorf("per-role policy should not be nil") + } + if err := validatePolicy(policy); err != nil { + return fmt.Errorf("invalid policy (role: %s): %w", role, err) + } + } + return nil } @@ -132,6 +157,25 @@ func (sc *SGXConstraints) ContainsEnclave(eid sgx.EnclaveIdentity) bool { return slices.Contains(sc.Enclaves, eid) } +// EffectivePolicy returns a combined policy. The combined policy may in addition to the default policy, +// (depending on the specified roles) also include additional per-role policies. +func (sc *SGXConstraints) EffectivePolicy(roles RolesMask) *quote.Policy { + effectivePolicy := sc.Policy // TODO: Make this a deep copy or better yet ensure Merge produces it. + if effectivePolicy == nil { + effectivePolicy = "e.Policy{} + } + + for role, policy := range sc.PerRolePolicy { + if role&roles == 0 { + continue + } + // We are ignoring deprecated IAS part, possibly we could error if if set twice. + effectivePolicy.PCS = effectivePolicy.PCS.Merge(policy.PCS) + } + + return effectivePolicy +} + const ( // LatestSGXAttestationVersion is the latest SGX attestation structure version that should be // used for all new descriptors. @@ -213,6 +257,7 @@ func (sa *SGXAttestation) ValidateBasic(cfg *TEEFeatures) error { } // Verify verifies the SGX attestation. +// TODO: Extensive test suite for this function that acts as integration test (consider mocking). func (sa *SGXAttestation) Verify( cfg *TEEFeatures, ts time.Time, @@ -220,7 +265,7 @@ func (sa *SGXAttestation) Verify( sc *SGXConstraints, rak signature.PublicKey, rek *x25519.PublicKey, - nodeID signature.PublicKey, + n *Node, ) error { if cfg == nil { cfg = &emptyFeatures @@ -229,8 +274,9 @@ func (sa *SGXAttestation) Verify( // Use defaults from consensus parameters. cfg.SGX.ApplyDefaultConstraints(sc) - // Verify the quote. - verifiedQuote, err := sa.Quote.Verify(sc.Policy, ts) + // Verify the quote againt the effective policy. + policy := sc.EffectivePolicy(n.Roles) + verifiedQuote, err := sa.Quote.Verify(policy, ts) if err != nil { return err } @@ -254,7 +300,7 @@ func (sa *SGXAttestation) Verify( if cfg.SGX.SignedAttestations { // In case the signed attestation feature is enabled, verify the signature. - return sa.verifyAttestationSignature(sc, rak, rek, verifiedQuote.ReportData, nodeID, height) + return sa.verifyAttestationSignature(sc, rak, rek, verifiedQuote.ReportData, n.ID, height) } return nil diff --git a/go/common/node/tee.go b/go/common/node/tee.go index 71acb0a84c6..a51c7775747 100644 --- a/go/common/node/tee.go +++ b/go/common/node/tee.go @@ -34,17 +34,13 @@ type TEEFeaturesSGX struct { // ApplyDefaultConstraints applies configured SGX constraint defaults to the given structure. func (fs *TEEFeaturesSGX) ApplyDefaultConstraints(sc *SGXConstraints) { - // Default policy. - if fs.DefaultPolicy != nil { - if sc.Policy == nil { - sc.Policy = "e.Policy{} - } - if sc.Policy.IAS == nil { - sc.Policy.IAS = fs.DefaultPolicy.IAS - } - if sc.Policy.PCS == nil && fs.PCS { - sc.Policy.PCS = fs.DefaultPolicy.PCS + sc.Policy = sc.Policy.ApplyDefault(fs.DefaultPolicy, fs.PCS) + + for role, policy := range sc.PerRolePolicy { + if policy == nil { + continue } + sc.PerRolePolicy[role] = policy.ApplyDefault(fs.DefaultPolicy, fs.PCS) } // Default maximum attestation age. diff --git a/go/common/sgx/pcs/policy.go b/go/common/sgx/pcs/policy.go index ba846fadacc..7a9681eb054 100644 --- a/go/common/sgx/pcs/policy.go +++ b/go/common/sgx/pcs/policy.go @@ -2,6 +2,8 @@ package pcs import ( "fmt" + "maps" + "slices" ) // QuotePolicy is the quote validity policy. @@ -28,6 +30,68 @@ type QuotePolicy struct { TDX *TdxQuotePolicy `json:"tdx,omitempty" yaml:"tdx,omitempty"` } +// Merge merges two QuotePolicies into one, taking more restrictive configuration into account. +// +// TODO: +// - What if FMSCPWhitelist has no intersection (same applies for TDXQuotePolicy)? +// - Should we even allow registration of such runtime descriptor? +// - Finish TDX quote policy merge (ugly). +// - Unit tests. +// - Merge should produce independent copies to not accidentally mutate stuff. +func (p *QuotePolicy) Merge(o *QuotePolicy) *QuotePolicy { + if p == nil { + return o + } + + if o == nil { + return p + } + + merged := &QuotePolicy{ + Disabled: p.Disabled || o.Disabled, + TCBValidityPeriod: min(p.TCBValidityPeriod, o.TCBValidityPeriod), + MinTCBEvaluationDataNumber: max(p.MinTCBEvaluationDataNumber, o.MinTCBEvaluationDataNumber), + } + + func() { + if len(p.FMSPCWhitelist) == 0 { + merged.FMSPCWhitelist = o.FMSPCWhitelist + return + } + + if len(o.FMSPCWhitelist) == 0 { + merged.FMSPCWhitelist = p.FMSPCWhitelist + return + } + + intersect := make(map[string]struct{}, len(p.FMSPCWhitelist)) + for _, fmspc := range p.FMSPCWhitelist { + intersect[fmspc] = struct{}{} + } + for _, fmspc := range o.FMSPCWhitelist { + if _, ok := intersect[fmspc]; ok { + merged.FMSPCWhitelist = append(merged.FMSPCWhitelist, fmspc) + } + } + + // Preventing no intersection meaning allow any. + if len(merged.FMSPCWhitelist) == 0 { + merged.Disabled = true + } + + }() + + union := make(map[string]struct{}, len(p.FMSPCBlacklist)+len(o.FMSPCBlacklist)) + for _, fmspc := range append(p.FMSPCBlacklist, o.FMSPCBlacklist...) { + union[fmspc] = struct{}{} + } + merged.FMSPCBlacklist = slices.Collect(maps.Keys(union)) + slices.Sort(merged.FMSPCBlacklist) + + merged.TDX = p.TDX.Merge(o.TDX) + return merged +} + // TdxQuotePolicy is the TDX-specific quote policy. type TdxQuotePolicy struct { // AllowedTdxModules are the allowed TDX modules. Empty to allow ANY Intel-signed module. @@ -56,6 +120,25 @@ func (tp *TdxQuotePolicy) verifyTdxModule(report *TdReport) error { return fmt.Errorf("pcs/quote: TDX module not allowed") } +func (tp *TdxQuotePolicy) Merge(o *TdxQuotePolicy) *TdxQuotePolicy { + if tp == nil || o == nil { + return nil + } + + if len(tp.AllowedTdxModules) == 0 { + return o + } + + if len(o.AllowedTdxModules) == 0 { + return tp + } + + // TODO + // Merge the TDXQuotePolicy. + + return o +} + // TDX_MrSigner_Intel is the TDX module MRSIGNER for Intel (000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000). var TDX_MrSigner_Intel [48]byte // nolint: revive diff --git a/go/common/sgx/quote/quote.go b/go/common/sgx/quote/quote.go index 0441987e51d..32ab4d4ad87 100644 --- a/go/common/sgx/quote/quote.go +++ b/go/common/sgx/quote/quote.go @@ -81,3 +81,22 @@ func (p *Policy) Validate(isFeatureVersion242 bool) error { return fmt.Errorf("fmspc whitelist should be empty") } + +func (p *Policy) ApplyDefault(d *Policy, pcsEnabled bool) *Policy { + if d == nil { + return p + } + + if p == nil { + p = &Policy{} + } + + if p.IAS == nil { + p.IAS = d.IAS + } + if p.PCS == nil && pcsEnabled { + p.PCS = d.PCS + } + + return p +} diff --git a/go/consensus/cometbft/apps/keymanager/secrets/status.go b/go/consensus/cometbft/apps/keymanager/secrets/status.go index fc978655dcd..cd269597e28 100644 --- a/go/consensus/cometbft/apps/keymanager/secrets/status.go +++ b/go/consensus/cometbft/apps/keymanager/secrets/status.go @@ -112,7 +112,7 @@ nextNode: continue nextNode } - initResponse, err := VerifyExtraInfo(ctx.Logger(), n.ID, kmrt, nodeRt, ts, height, params, isFeatureVersion242) + initResponse, err := VerifyExtraInfo(ctx.Logger(), n, kmrt, nodeRt, ts, height, params, isFeatureVersion242) if err != nil { ctx.Logger().Error("failed to validate ExtraInfo", append(vars, "err", err)...) continue nextNode @@ -226,7 +226,7 @@ nextNode: // blob for a key manager. func VerifyExtraInfo( logger *logging.Logger, - nodeID signature.PublicKey, + n *node.Node, rt *registry.Runtime, nodeRt *node.Runtime, ts time.Time, @@ -234,7 +234,7 @@ func VerifyExtraInfo( params *registry.ConsensusParameters, isFeatureVersion242 bool, ) (*secrets.InitResponse, error) { - if err := registry.VerifyNodeRuntimeEnclaveIDs(logger, nodeID, nodeRt, rt, params.TEEFeatures, ts, height, isFeatureVersion242); err != nil { + if err := registry.VerifyNodeRuntimeEnclaveIDs(logger, n, nodeRt, rt, params.TEEFeatures, ts, height, isFeatureVersion242); err != nil { return nil, err } if nodeRt.ExtraInfo == nil { diff --git a/go/consensus/cometbft/apps/scheduler/scheduler.go b/go/consensus/cometbft/apps/scheduler/scheduler.go index 7f354e152ad..cb450e036b1 100644 --- a/go/consensus/cometbft/apps/scheduler/scheduler.go +++ b/go/consensus/cometbft/apps/scheduler/scheduler.go @@ -412,7 +412,7 @@ func isSuitableExecutorWorker( ctx.Now(), uint64(ctx.LastHeight()), activeDeployment.TEE, - n.node.ID, + n.node, isFeatureVersion242, ); err != nil { ctx.Logger().Warn("failed to verify node TEE attestation", diff --git a/go/oasis-node/cmd/debug/byzantine/steps_test.go b/go/oasis-node/cmd/debug/byzantine/steps_test.go index 38e1afb7645..957c6e8b709 100644 --- a/go/oasis-node/cmd/debug/byzantine/steps_test.go +++ b/go/oasis-node/cmd/debug/byzantine/steps_test.go @@ -7,15 +7,14 @@ import ( "github.com/stretchr/testify/require" "github.com/oasisprotocol/oasis-core/go/common/cbor" - "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/node" "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/sgx/ias" ) func TestFakeCapabilitySGX(t *testing.T) { - var nodeID signature.PublicKey - _, fakeCapabilitiesSGX, err := initFakeCapabilitiesSGX(nodeID) + var n node.Node + _, fakeCapabilitiesSGX, err := initFakeCapabilitiesSGX(n.ID) require.NoError(t, err, "initFakeCapabilitiesSGX failed") cs := cbor.Marshal(node.SGXConstraints{ @@ -31,5 +30,5 @@ func TestFakeCapabilitySGX(t *testing.T) { ias.SetSkipVerify() ias.SetAllowDebugEnclaves() - require.NoError(t, fakeCapabilitiesSGX.TEE.Verify(&teeCfg, time.Now(), 1, cs, nodeID, true), "fakeCapabilitiesSGX not valid") + require.NoError(t, fakeCapabilitiesSGX.TEE.Verify(&teeCfg, time.Now(), 1, cs, &n, true), "fakeCapabilitiesSGX not valid") } diff --git a/go/registry/api/api.go b/go/registry/api/api.go index 2811a120fac..bdb4ff02694 100644 --- a/go/registry/api/api.go +++ b/go/registry/api/api.go @@ -619,8 +619,10 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo // both validators and compute nodes and have out of date attestation evidence. Removing // such nodes could lead to consensus not having the proper majority. This is safe as // attestation evidence is independently verified before scheduling committees. - if err := VerifyNodeRuntimeEnclaveIDs(logger, n.ID, rt, regRt, params.TEEFeatures, now, height, isFeatureVersion242); err != nil && !isSanityCheck && !isGenesis { - return nil, nil, err + if !isSanityCheck && !isGenesis { + if err := VerifyNodeRuntimeEnclaveIDs(logger, &n, rt, regRt, params.TEEFeatures, now, height, isFeatureVersion242); err != nil { + return nil, nil, err + } } // Enforce what kinds of runtimes are allowed. @@ -795,7 +797,7 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo // VerifyNodeRuntimeEnclaveIDs verifies TEE-specific attributes of the node's runtime. func VerifyNodeRuntimeEnclaveIDs( logger *logging.Logger, - nodeID signature.PublicKey, + n *node.Node, rt *node.Runtime, regRt *Runtime, teeCfg *node.TEEFeatures, @@ -830,9 +832,9 @@ func VerifyNodeRuntimeEnclaveIDs( continue } - if err := rt.Capabilities.TEE.Verify(teeCfg, ts, height, rtVersionInfo.TEE, nodeID, isFeatureVersion242); err != nil { + if err := rt.Capabilities.TEE.Verify(teeCfg, ts, height, rtVersionInfo.TEE, n, isFeatureVersion242); err != nil { logger.Error("VerifyNodeRuntimeEnclaveIDs: failed to validate attestation", - "node_id", nodeID, + "node_id", n.ID, "runtime_id", rt.ID, "ts", ts, "err", err, diff --git a/go/runtime/host/protocol/types.go b/go/runtime/host/protocol/types.go index 2ece8d9f53d..93e52f72adc 100644 --- a/go/runtime/host/protocol/types.go +++ b/go/runtime/host/protocol/types.go @@ -659,4 +659,6 @@ type HostIdentityRequest struct{} type HostIdentityResponse struct { // NodeID is the host node identifier. NodeID signature.PublicKey `json:"node_id"` + // Roles is the host node role mask. + Roles node.RolesMask `json:"roles,omitempty"` } diff --git a/go/runtime/host/sgx/common/common.go b/go/runtime/host/sgx/common/common.go index 51568a842b1..6bfcea757cd 100644 --- a/go/runtime/host/sgx/common/common.go +++ b/go/runtime/host/sgx/common/common.go @@ -48,6 +48,16 @@ func GetQuotePolicy( return nil, fmt.Errorf("malformed runtime SGX constraints: %w", err) } + // TODO: We should return effective quote policy here. This function is called when node + // is creating a TCBBundle. Technically attestation will be verified on both consensus and + // runtime site but we want to catch problems early-on to avoid failed transaction. + // + // Crux: host config does not have access to the registration worker (role providers) roles. + // + // Possible solutions: + // 1. Map config to consensus registry roles(modes &| configured identity &| consesus.validator |& storage.public_rpc_enabled). + // 2. Delay this validation and call it from the registration worker just before submitting registration with attestation. + // 3. Somehow pass role provider roles here. return sc.Policy, nil } return fallbackPolicy, nil diff --git a/go/runtime/registry/handler.go b/go/runtime/registry/handler.go index 1b059d0633e..7b0cdca50d2 100644 --- a/go/runtime/registry/handler.go +++ b/go/runtime/registry/handler.go @@ -7,6 +7,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/identity" + "github.com/oasisprotocol/oasis-core/go/common/node" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" "github.com/oasisprotocol/oasis-core/go/consensus/api/transaction" consensusResults "github.com/oasisprotocol/oasis-core/go/consensus/api/transaction/results" @@ -115,7 +116,7 @@ func (h *runtimeHostHandler) Handle(ctx context.Context, rq *protocol.Body) (*pr rsp.HostProveFreshnessResponse, err = h.handleHostProveFreshness(ctx, rq.HostProveFreshnessRequest) case rq.HostIdentityRequest != nil: // Host identity. - rsp.HostIdentityResponse, err = h.handleHostIdentity() + rsp.HostIdentityResponse, err = h.handleHostIdentity(ctx) default: err = fmt.Errorf("method not supported") } @@ -438,13 +439,17 @@ func (h *runtimeHostHandler) handleHostProveFreshness( }, nil } -func (h *runtimeHostHandler) handleHostIdentity() (*protocol.HostIdentityResponse, error) { +func (h *runtimeHostHandler) handleHostIdentity(ctx context.Context) (*protocol.HostIdentityResponse, error) { identity, err := h.env.GetNodeIdentity() if err != nil { return nil, err } + // TODO: Populate roles via environment. Similar problem to common.GetQuotePolicy, i.e. + // we don't have access to role provider and we either have to find a mapping from config to + // the roles we are registering for or find a way to get them. return &protocol.HostIdentityResponse{ NodeID: identity.NodeSigner.Public(), + Roles: node.RoleEmpty, }, nil } diff --git a/go/runtime/registry/notifier_policy.go b/go/runtime/registry/notifier_policy.go index 8c166da2f47..960916978ff 100644 --- a/go/runtime/registry/notifier_policy.go +++ b/go/runtime/registry/notifier_policy.go @@ -165,9 +165,11 @@ func (n *KeyManagerNotifier) watchKmPolicyUpdates(ctx context.Context, kmRtID *c // Make sure that we actually have a new quote policy and that the current runtime version // supports quote policy updates. - if !quotePolicyUpdated && sc != nil && sc.Policy != nil { - n.updateKeyManagerQuotePolicy(sc.Policy) - quotePolicyUpdated = true + if !quotePolicyUpdated && sc != nil { // Possible nil vs empty semantic change (check if it matters). + if policy := sc.EffectivePolicy(node.RoleKeyManager); policy != nil { + n.updateKeyManagerQuotePolicy(policy) + quotePolicyUpdated = true + } } select { diff --git a/runtime/src/attestation.rs b/runtime/src/attestation.rs index 9098d582b83..833ee70b575 100644 --- a/runtime/src/attestation.rs +++ b/runtime/src/attestation.rs @@ -113,9 +113,15 @@ impl Handler { let consensus_verifier = self.consensus_verifier.clone(); let version = self.version; let runtime_id = self.runtime_id; + // TODO: Chicken and an egg problem: enclave should verify quote prior + // to being registered, but this means it cannot fetch roles from the consensus, + // meaning it trust the host that it should not? Similar problem is for the + // node_id? + let (_, roles) = self.host.identity().await?; tokio::task::block_in_place(move || { // Obtain current quote policy from (verified) consensus state. - PolicyVerifier::new(consensus_verifier).quote_policy(&runtime_id, Some(version)) + PolicyVerifier::new(consensus_verifier) + .effective_quote_policy(&runtime_id, Some(version), roles) })? }; @@ -137,7 +143,7 @@ impl Handler { ); // Configure the quote and policy on the identity. - let node_id = self.host.identity().await?; + let (node_id, _) = self.host.identity().await?; let verified_quote = self.identity.set_quote(node_id, quote)?; // Sign the report data, latest verified consensus height, REK and host node ID. diff --git a/runtime/src/consensus/registry.rs b/runtime/src/consensus/registry.rs index 50f03541731..82e56677ca2 100644 --- a/runtime/src/consensus/registry.rs +++ b/runtime/src/consensus/registry.rs @@ -699,10 +699,14 @@ pub enum SGXConstraints { #[cbor(optional)] enclaves: Vec, - /// The quote policy. + /// Default quote policy. #[cbor(optional)] policy: sgx::QuotePolicy, + /// Additional per-role quote policies. + #[cbor(optional)] + per_role_policy: BTreeMap, + /// The maximum attestation age (in blocks). #[cbor(optional)] max_attestation_age: u64, @@ -741,6 +745,13 @@ impl SGXConstraints { Self::V1 { ref policy, .. } => policy.clone(), } } + + /// Effective SGX quote policy for the given roles. + /// + /// TODO: Implement effective policy and underyling merging. + pub fn effective_policy(&self, roles: RolesMask) -> sgx::QuotePolicy { + self.policy() + } } /// Verified remote attestation. diff --git a/runtime/src/consensus/tendermint/verifier/mod.rs b/runtime/src/consensus/tendermint/verifier/mod.rs index 2fb65fc057c..0f4ef67135f 100644 --- a/runtime/src/consensus/tendermint/verifier/mod.rs +++ b/runtime/src/consensus/tendermint/verifier/mod.rs @@ -707,7 +707,7 @@ impl Verifier { "trust_root_chain_context" => ?trust_root.chain_context, ); - let host_node_id = + let (host_node_id, _) = block_on(self.protocol.identity()).expect("host should provide a node identity"); let mut cache = Cache::new(host_node_id); diff --git a/runtime/src/dispatcher.rs b/runtime/src/dispatcher.rs index 3d1baade1b2..a51f523fddc 100644 --- a/runtime/src/dispatcher.rs +++ b/runtime/src/dispatcher.rs @@ -21,6 +21,7 @@ use crate::{ }, consensus::{ beacon::EpochTime, + registry::RolesMask, roothash::{self, ComputeResultsHeader, Header, COMPUTE_RESULTS_HEADER_SIGNATURE_CONTEXT}, state::keymanager::Status as KeyManagerStatus, verifier::Verifier, @@ -988,12 +989,25 @@ impl Dispatcher { // Verify and decode the policy. let runtime_id = state.protocol.get_host_info().runtime_id; + // TODO: Why are we passing the quote policy from the host given that it needs to be fetched + // using the consensus verifier anyways. This could be signal only (empty struct)? It would be breaking + // to change the signature now, but technically we could ignore it. + // + // Alternative, would be to change the host callsite to only dummy policy, ignore it here and fetch other policies here. + // This way we could get rid of effective policy all-together and keep passing slices of policies, but this would change + // many function signatures, including sessions update policy, so not sure we profit at all. + tokio::task::spawn_blocking(move || -> Result<(), Error> { let key_manager = state.policy_verifier.key_manager(&runtime_id)?; let policy = state .policy_verifier - .verify_quote_policy(quote_policy, &key_manager, None)?; + .verify_quote_policy( + quote_policy, + &key_manager, + None, + RolesMask::ROLE_KEY_MANAGER, + )?; // Dispatch the local RPC call. state.rpc_dispatcher.handle_km_quote_policy_update(policy); diff --git a/runtime/src/enclave_rpc/session.rs b/runtime/src/enclave_rpc/session.rs index 97f447a263e..76691908ba3 100644 --- a/runtime/src/enclave_rpc/session.rs +++ b/runtime/src/enclave_rpc/session.rs @@ -245,6 +245,7 @@ impl Session { return Ok(None); } + // TODO: Ensure correctly propagated per-role policy is in the session config. let policy = self .cfg .policy diff --git a/runtime/src/host/mod.rs b/runtime/src/host/mod.rs index 3edf8384a66..919917021e9 100644 --- a/runtime/src/host/mod.rs +++ b/runtime/src/host/mod.rs @@ -4,6 +4,7 @@ use thiserror::Error; use crate::{ common::{crypto::signature::PublicKey, namespace::Namespace}, + consensus::registry::RolesMask, enclave_rpc, protocol::Protocol, storage::mkvs::sync, @@ -56,7 +57,7 @@ pub struct TxResult { #[async_trait] pub trait Host: Send + Sync { /// Returns the identity of the host node. - async fn identity(&self) -> Result; + async fn identity(&self) -> Result<(PublicKey, RolesMask), Error>; /// Submit a transaction. async fn submit_tx(&self, data: Vec, opts: SubmitTxOpts) @@ -77,9 +78,11 @@ pub trait Host: Send + Sync { #[async_trait] impl Host for Protocol { - async fn identity(&self) -> Result { + async fn identity(&self) -> Result<(PublicKey, RolesMask), Error> { match self.call_host_async(Body::HostIdentityRequest {}).await? { - Body::HostIdentityResponse { node_id } => Ok(node_id), + Body::HostIdentityResponse { node_id, roles } => { + Ok((node_id, roles.unwrap_or(RolesMask::ROLE_EMPTY))) + } _ => Err(Error::BadResponse), } } diff --git a/runtime/src/policy.rs b/runtime/src/policy.rs index b126be68532..3aea831ecd5 100644 --- a/runtime/src/policy.rs +++ b/runtime/src/policy.rs @@ -10,7 +10,7 @@ use crate::{ common::{logger::get_logger, namespace::Namespace, sgx::QuotePolicy, version::Version}, consensus::{ keymanager::SignedPolicySGX, - registry::{SGXConstraints, TEEHardware}, + registry::{RolesMask, SGXConstraints, TEEHardware}, state::{ beacon::ImmutableState as BeaconState, keymanager::{ImmutableState as KeyManagerState, Status}, @@ -60,13 +60,14 @@ impl PolicyVerifier { } } - /// Fetch runtime's quote policy from the latest verified consensus layer state. + /// Fetch runtime's effective quote policy from the latest verified consensus layer state. /// /// If the runtime version is not provided, the policy for the active deployment is returned. - pub fn quote_policy( + pub fn effective_quote_policy( &self, runtime_id: &Namespace, version: Option, + roles: RolesMask, ) -> Result { // Fetch quote policy from the consensus layer using the given or the active version. // TODO: Make this async. @@ -95,7 +96,7 @@ impl PolicyVerifier { let sc: SGXConstraints = ad .try_decode_tee() .map_err(|_| PolicyVerifierError::BadTEEConstraints)?; - sc.policy() + sc.effective_policy(roles) } _ => bail!(PolicyVerifierError::HardwareMismatch), }; @@ -103,14 +104,15 @@ impl PolicyVerifier { Ok(policy) } - /// Verify that runtime's quote policy has been published in the consensus layer. + /// Verify that runtime's effective quote policy has been published in the consensus layer. pub fn verify_quote_policy( &self, policy: QuotePolicy, runtime_id: &Namespace, version: Option, + roles: RolesMask, ) -> Result { - let published_policy = self.quote_policy(runtime_id, version)?; + let published_policy = self.effective_quote_policy(runtime_id, version, roles)?; if policy != published_policy { debug!( diff --git a/runtime/src/types.rs b/runtime/src/types.rs index 1adcf074cc9..61d773da1ca 100644 --- a/runtime/src/types.rs +++ b/runtime/src/types.rs @@ -17,7 +17,7 @@ use crate::{ consensus::{ self, beacon::EpochTime, - registry::EndorsedCapabilityTEE, + registry::{EndorsedCapabilityTEE, RolesMask}, roothash::{self, Block, ComputeResultsHeader, Header}, state::keymanager::Status as KeyManagerStatus, transaction::{Proof, SignedTransaction}, @@ -301,6 +301,8 @@ pub enum Body { HostIdentityRequest {}, HostIdentityResponse { node_id: signature::PublicKey, + #[cbor(optional)] + roles: Option, }, HostSubmitTxRequest { runtime_id: Namespace,