Skip to content

Commit

Permalink
Merge pull request #5539 from oasisprotocol/peternose/feature/authori…
Browse files Browse the repository at this point in the history
…ze-connect-calls

go/worker/keymanager: Authorize noise session connect calls
  • Loading branch information
peternose authored Jan 25, 2024
2 parents 094f5e5 + 7a8a4d6 commit 6d6b042
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 80 deletions.
4 changes: 4 additions & 0 deletions .changelog/5539.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
go/worker/keymanager: Authorize noise session connect calls

A peer is granted permission to connect if it is authorized
to invoke at least one secure enclave RPC method.
3 changes: 3 additions & 0 deletions go/keymanager/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ var (
MethodPublishEphemeralSecret,
}

// RPCMethodConnect is the name of the method used to establish a Noise session.
RPCMethodConnect = ""

// RPCMethodInit is the name of the `init` method.
RPCMethodInit = "init"

Expand Down
4 changes: 4 additions & 0 deletions go/worker/keymanager/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ type RPCAccessController interface {
// Methods returns a list of allowed methods.
Methods() []string

// Connect verifies whether the peer is allowed to establish a secure Noise connection,
// meaning it is authorized to invoke at least one secure RPC method.
Connect(peerID core.PeerID) bool

// Authorize verifies whether the peer is allowed to invoke the specified RPC method.
Authorize(method string, kind enclaverpc.Kind, peerID core.PeerID) error
}
15 changes: 9 additions & 6 deletions go/worker/keymanager/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,16 @@ func New(
return nil, fmt.Errorf("worker/keymanager: failed to create secrets worker: %w", err)
}

// Register methods.
w.accessControllers = make(map[string]workerKeymanager.RPCAccessController)
for _, m := range w.secretsWorker.Methods() {
if _, ok := w.accessControllers[m]; ok {
return nil, fmt.Errorf("worker/keymanager: duplicate enclave RPC method: %s", m)
// Prepare access controllers and register their methods.
w.accessControllers = []workerKeymanager.RPCAccessController{w.secretsWorker}
w.accessControllersByMethod = make(map[string]workerKeymanager.RPCAccessController)
for _, ctrl := range w.accessControllers {
for _, m := range ctrl.Methods() {
if _, ok := w.accessControllersByMethod[m]; ok {
return nil, fmt.Errorf("worker/keymanager: duplicate enclave RPC method: %s", m)
}
w.accessControllersByMethod[m] = w.secretsWorker
}
w.accessControllers[m] = w.secretsWorker
}

// Register keymanager service.
Expand Down
155 changes: 92 additions & 63 deletions go/worker/keymanager/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/cenkalti/backoff/v4"
"github.com/libp2p/go-libp2p/core"
"golang.org/x/exp/maps"

beacon "github.com/oasisprotocol/oasis-core/go/beacon/api"
"github.com/oasisprotocol/oasis-core/go/common"
Expand Down Expand Up @@ -41,6 +42,18 @@ const (
ephemeralSecretCacheSize = 20
)

var insecureRPCMethods = map[string]struct{}{
api.RPCMethodGetPublicKey: {},
api.RPCMethodGetPublicEphemeralKey: {},
}

var secureRPCMethods = map[string]struct{}{
api.RPCMethodGetOrCreateKeys: {},
api.RPCMethodGetOrCreateEphemeralKeys: {},
api.RPCMethodReplicateMasterSecret: {},
api.RPCMethodReplicateEphemeralSecret: {},
}

// Ensure the secrets worker implements the RPCAccessController interface.
var _ workerKm.RPCAccessController = (*secretsWorker)(nil)

Expand Down Expand Up @@ -145,105 +158,121 @@ func newSecretsWorker(

// Methods implements RPCAccessController interface.
func (w *secretsWorker) Methods() []string {
return []string{
api.RPCMethodInit,
api.RPCMethodGetOrCreateKeys,
api.RPCMethodGetPublicKey,
api.RPCMethodGetOrCreateEphemeralKeys,
api.RPCMethodGetPublicEphemeralKey,
api.RPCMethodReplicateMasterSecret,
api.RPCMethodReplicateEphemeralSecret,
api.RPCMethodGenerateMasterSecret,
api.RPCMethodGenerateEphemeralSecret,
api.RPCMethodLoadMasterSecret,
api.RPCMethodLoadEphemeralSecret,
var methods []string
methods = append(methods, maps.Keys(secureRPCMethods)...)
methods = append(methods, maps.Keys(insecureRPCMethods)...)
return methods
}

// Connect implements RPCAccessController interface.
func (w *secretsWorker) Connect(peerID core.PeerID) bool {
// Start accepting requests after initialization.
w.mu.RLock()
state := w.status.Worker.Status
kmStatus := w.status.Status
w.mu.RUnlock()

if state != workerKm.StatusStateReady || kmStatus == nil {
return false
}

// Secure methods are accessible to private peers without restrictions.
if _, ok := w.privatePeers[peerID]; ok {
return true
}

// Other peers must undergo the authorization process.
if err := w.authorizeNode(peerID, kmStatus); err == nil {
return true
}
if err := w.authorizeKeyManager(peerID); err == nil {
return true
}

return false
}

// Authorize implements RPCAccessController interface.
func (w *secretsWorker) Authorize(method string, kind enclaverpc.Kind, peerID core.PeerID) error {
// Start accepting requests after initialization.
w.mu.RLock()
status := w.status.Worker.Status
state := w.status.Worker.Status
kmStatus := w.status.Status
w.mu.RUnlock()

if status != workerKm.StatusStateReady {
if state != workerKm.StatusStateReady || kmStatus == nil {
return fmt.Errorf("not initialized")
}

// Check if the method is supported.
switch kind {
case enclaverpc.KindInsecureQuery:
switch method {
case api.RPCMethodGetPublicKey:
case api.RPCMethodGetPublicEphemeralKey:
default:
if _, ok := insecureRPCMethods[method]; !ok {
return fmt.Errorf("unsupported method: %s", method)
}
return nil
case enclaverpc.KindNoiseSession:
switch method {
case api.RPCMethodGetOrCreateKeys:
case api.RPCMethodGetOrCreateEphemeralKeys:
case api.RPCMethodReplicateMasterSecret:
case api.RPCMethodReplicateEphemeralSecret:
default:
if _, ok := secureRPCMethods[method]; !ok {
return fmt.Errorf("unsupported method: %s", method)
}
default:
return fmt.Errorf("unsupported kind: %s", kind)
}

// Secure methods are accessible to private peers without restrictions.
if _, ok := w.privatePeers[peerID]; ok {
return nil
}

w.mu.RLock()
kmStatus := w.status.Status
w.mu.RUnlock()
// Other peers must undergo the authorization process.
switch method {
case api.RPCMethodGetOrCreateKeys, api.RPCMethodGetOrCreateEphemeralKeys:
return w.authorizeNode(peerID, kmStatus)
case api.RPCMethodReplicateMasterSecret, api.RPCMethodReplicateEphemeralSecret:
return w.authorizeKeyManager(peerID)
default:
return fmt.Errorf("unsupported method: %s", method)
}
}

if kmStatus == nil || !kmStatus.IsInitialized {
return fmt.Errorf("not initialized")
func (w *secretsWorker) authorizeNode(peerID core.PeerID, kmStatus *api.Status) error {
capabilityTEE, err := w.kmWorker.GetHostedRuntimeCapabilityTEE()
if err != nil {
return err
}

switch method {
case api.RPCMethodGetOrCreateKeys, api.RPCMethodGetOrCreateEphemeralKeys:
capabilityTEE, err := w.kmWorker.GetHostedRuntimeCapabilityTEE()
if err != nil {
return err
switch {
case capabilityTEE == nil:
// Insecure key manager enclaves can be queried by all runtimes (used for testing).
if kmStatus.IsSecure {
return fmt.Errorf("untrusted hardware")
}

switch {
case capabilityTEE == nil:
// Insecure key manager enclaves can be queried by all runtimes (used for testing).
if w.status.Status.IsSecure {
return fmt.Errorf("untrusted hardware")
}
return nil
case capabilityTEE.Hardware == node.TEEHardwareIntelSGX:
// Secure key manager enclaves can be queried by runtimes specified in the policy.
if w.status.Status.Policy == nil {
return fmt.Errorf("policy not set")
}
rts := w.kmWorker.accessList.Runtimes(peerID)
for _, enc := range w.status.Status.Policy.Policy.Enclaves { // TODO: Use the right enclave identity.
for rtID := range enc.MayQuery {
if rts.Contains(rtID) {
return nil
}
return nil
case capabilityTEE.Hardware == node.TEEHardwareIntelSGX:
// Secure key manager enclaves can be queried by runtimes specified in the policy.
if kmStatus.Policy == nil {
return fmt.Errorf("policy not set")
}
rts := w.kmWorker.accessList.Runtimes(peerID)
for _, enc := range kmStatus.Policy.Policy.Enclaves { // TODO: Use the right enclave identity.
for rtID := range enc.MayQuery {
if rts.Contains(rtID) {
return nil
}
}
return fmt.Errorf("query not allowed")
default:
return fmt.Errorf("unsupported hardware: %s", capabilityTEE.Hardware)
}
case api.RPCMethodReplicateMasterSecret, api.RPCMethodReplicateEphemeralSecret:
// Replication is restricted to peers within the same key manager runtime.
if !w.kmWorker.accessList.Runtimes(peerID).Contains(w.runtimeID) {
return fmt.Errorf("not a key manager")
}
return nil
return fmt.Errorf("query not allowed")
default:
return fmt.Errorf("unsupported method: %s", method)
return fmt.Errorf("unsupported hardware: %s", capabilityTEE.Hardware)
}
}

func (w *secretsWorker) authorizeKeyManager(peerID core.PeerID) error {
// Allow only peers within the same key manager runtime.
if !w.kmWorker.accessList.Runtimes(peerID).Contains(w.runtimeID) {
return fmt.Errorf("not a key manager")
}
return nil
}

// Initialized returns a channel that will be closed when the worker is initialized
Expand Down
33 changes: 22 additions & 11 deletions go/worker/keymanager/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ type Worker struct { // nolint: maligned
kmRuntimeWatcher *kmRuntimeWatcher
secretsWorker *secretsWorker

accessControllers map[string]workerKeymanager.RPCAccessController
accessControllers []workerKeymanager.RPCAccessController
accessControllersByMethod map[string]workerKeymanager.RPCAccessController

accessList *AccessList

Expand Down Expand Up @@ -145,20 +146,25 @@ func (w *Worker) CallEnclave(ctx context.Context, data []byte, kind enclaverpc.K
}

// Handle access control.
peerID, ok := rpc.PeerIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("not authorized: unknown peer")
}

switch {
case method == "" && kind == enclaverpc.KindNoiseSession:
// Anyone can connect.
default:
peerID, ok := rpc.PeerIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("not authorized: unknown peer")
case method == api.RPCMethodConnect && kind == enclaverpc.KindNoiseSession:
// Allow connection if at least one controller grants authorization.
fn := func(ctrl workerKeymanager.RPCAccessController) bool {
return ctrl.Connect(peerID)
}

ctrl, ok := w.accessControllers[method]
if !slices.ContainsFunc(w.accessControllers, fn) {
return nil, fmt.Errorf("not authorized to connect")
}
default:
ctrl, ok := w.accessControllersByMethod[method]
if !ok {
return nil, fmt.Errorf("unsupported RPC method")
}

if err := ctrl.Authorize(method, kind, peerID); err != nil {
return nil, fmt.Errorf("not authorized: %w", err)
}
Expand All @@ -174,8 +180,10 @@ func (w *Worker) CallEnclave(ctx context.Context, data []byte, kind enclaverpc.K
},
}

// Hosted runtime should not be nil as we are initialized.
rt := w.GetHostedRuntime()
if rt == nil {
return nil, fmt.Errorf("not initialized")
}
response, err := rt.Call(ctx, req)
if err != nil {
w.logger.Error("failed to dispatch RPC call to runtime",
Expand Down Expand Up @@ -208,6 +216,9 @@ func (w *Worker) callEnclaveLocal(method string, args interface{}, rsp interface
}

rt := w.GetHostedRuntime()
if rt == nil {
return fmt.Errorf("not initialized")
}
response, err := rt.Call(w.ctx, body)
if err != nil {
w.logger.Error("failed to dispatch local RPC call to runtime",
Expand Down

0 comments on commit 6d6b042

Please sign in to comment.