Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/6387.breaking.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions go/common/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand Down
66 changes: 56 additions & 10 deletions go/common/node/sgx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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
}

Expand All @@ -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 = &quote.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.
Expand Down Expand Up @@ -213,14 +257,15 @@ 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,
height uint64,
sc *SGXConstraints,
rak signature.PublicKey,
rek *x25519.PublicKey,
nodeID signature.PublicKey,
n *Node,
Copy link
Contributor Author

@martintomazic martintomazic Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Followed the pattern from the admission policy. Alternative is to pass nodeID and roles (bitmask) by value.

Passing whole struct by reference feels dangerous as accidental mutations in the helper functions (possibly in the future) may affect the logic upstream.

) error {
if cfg == nil {
cfg = &emptyFeatures
Expand All @@ -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
}
Expand All @@ -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
Expand Down
16 changes: 6 additions & 10 deletions go/common/node/tee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = &quote.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.
Expand Down
83 changes: 83 additions & 0 deletions go/common/sgx/pcs/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package pcs

import (
"fmt"
"maps"
"slices"
)

// QuotePolicy is the quote validity policy.
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions go/common/sgx/quote/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 3 additions & 3 deletions go/consensus/cometbft/apps/keymanager/secrets/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -226,15 +226,15 @@ 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,
height uint64,
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 {
Expand Down
2 changes: 1 addition & 1 deletion go/consensus/cometbft/apps/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 3 additions & 4 deletions go/oasis-node/cmd/debug/byzantine/steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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")
}
12 changes: 7 additions & 5 deletions go/registry/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions go/runtime/host/protocol/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Loading
Loading