From 131b5d82bcaac23010ae1ffd1e0f0be99614c789 Mon Sep 17 00:00:00 2001 From: Paul Meyer <49727155+katexochen@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:23:38 +0200 Subject: [PATCH] recovery: add server and recover authority Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com> --- coordinator/internal/authority/authority.go | 18 ++ coordinator/main.go | 23 ++- coordinator/recoveryapi.go | 88 ++++++++ internal/recoveryapi/recoveryapi.go | 9 + internal/recoveryapi/recoveryapi.pb.go | 214 ++++++++++++++++++++ internal/recoveryapi/recoveryapi.proto | 16 ++ internal/recoveryapi/recoveryapi_grpc.pb.go | 109 ++++++++++ 7 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 coordinator/recoveryapi.go create mode 100644 internal/recoveryapi/recoveryapi.go create mode 100644 internal/recoveryapi/recoveryapi.pb.go create mode 100644 internal/recoveryapi/recoveryapi.proto create mode 100644 internal/recoveryapi/recoveryapi_grpc.pb.go diff --git a/coordinator/internal/authority/authority.go b/coordinator/internal/authority/authority.go index 537e9225e..90c90fef8 100644 --- a/coordinator/internal/authority/authority.go +++ b/coordinator/internal/authority/authority.go @@ -329,6 +329,24 @@ func (m *Authority) LatestManifest() (*manifest.Manifest, error) { return c.manifest, nil } +// Recoverable returns whether the Authority can be recovered from a persisted state. +func (m *Authority) Recoverable() (bool, error) { + return m.hist.HasLatest() +} + +// Recover recovers the seed engine from a seed and salt. +func (m *Authority) Recover(seed, salt []byte) error { + seedEngine, err := seedengine.New(seed, salt) + if err != nil { + return fmt.Errorf("creating seed engine: %w", err) + } + if !m.se.CompareAndSwap(nil, seedEngine) { + return errors.New("already recovered") + } + m.hist.ConfigureSigningKey(m.se.Load().TransactionSigningKey()) + return nil +} + // createSeedEngine populates m.se. // // It is fine to call this function concurrently. After it returns, m.se is guaranteed to be diff --git a/coordinator/main.go b/coordinator/main.go index 1cc933c8c..d8662f628 100644 --- a/coordinator/main.go +++ b/coordinator/main.go @@ -14,6 +14,7 @@ import ( "github.com/edgelesssys/contrast/coordinator/internal/authority" "github.com/edgelesssys/contrast/internal/logger" "github.com/edgelesssys/contrast/internal/meshapi" + "github.com/edgelesssys/contrast/internal/recoveryapi" "github.com/edgelesssys/contrast/internal/userapi" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -49,19 +50,39 @@ func run() (retErr error) { } metricsPort := os.Getenv(metricsPortEnvVar) - promRegistry := prometheus.NewRegistry() hist, err := history.New() if err != nil { return fmt.Errorf("creating history: %w", err) } + meshAuth := authority.New(hist, promRegistry, logger) + recoveryAPI := newRecoveryAPIServer(meshAuth, promRegistry, logger) userAPI := newUserAPIServer(meshAuth, promRegistry, logger) meshAPI := newMeshAPIServer(meshAuth, meshAuth, promRegistry, logger) eg := errgroup.Group{} + recoverable, err := meshAuth.Recoverable() + if err != nil { + return fmt.Errorf("checking recoverability: %w", err) + } + if recoverable { + logger.Warn("Coordinator is in recovery mode") + + eg.Go(func() error { + logger.Info("Coordinator recovery API listening") + if err := recoveryAPI.Serve(net.JoinHostPort("0.0.0.0", recoveryapi.Port)); err != nil { + return fmt.Errorf("serving recovery API: %w", err) + } + return nil + }) + + recoveryAPI.WaitRecoveryDone() + logger.Info("Coordinator recovery done") + } + eg.Go(func() error { if metricsPort == "" { return nil diff --git a/coordinator/recoveryapi.go b/coordinator/recoveryapi.go new file mode 100644 index 000000000..9bd9ba7a0 --- /dev/null +++ b/coordinator/recoveryapi.go @@ -0,0 +1,88 @@ +// Copyright 2024 Edgeless Systems GmbH +// SPDX-License-Identifier: AGPL-3.0-only + +package main + +import ( + "context" + "fmt" + "log/slog" + "net" + "time" + + "github.com/edgelesssys/contrast/internal/attestation/snp" + "github.com/edgelesssys/contrast/internal/grpc/atlscredentials" + "github.com/edgelesssys/contrast/internal/logger" + "github.com/edgelesssys/contrast/internal/recoveryapi" + grpcprometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" +) + +type recoveryAPIServer struct { + grpc *grpc.Server + logger *slog.Logger + recoverable recoverable + recoveryDoneC chan struct{} + + recoveryapi.UnimplementedRecoveryAPIServer +} + +func newRecoveryAPIServer(recoveryTarget recoverable, reg *prometheus.Registry, log *slog.Logger) *recoveryAPIServer { + issuer := snp.NewIssuer(logger.NewNamed(log, "snp-issuer")) + credentials := atlscredentials.New(issuer, nil) + + grpcUserAPIMetrics := grpcprometheus.NewServerMetrics( + grpcprometheus.WithServerCounterOptions( + grpcprometheus.WithSubsystem("contrast_recoveryapi"), + ), + grpcprometheus.WithServerHandlingTimeHistogram( + grpcprometheus.WithHistogramSubsystem("contrast_recoveryapi"), + grpcprometheus.WithHistogramBuckets([]float64{0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2.5, 5}), + ), + ) + + grpcServer := grpc.NewServer( + grpc.Creds(credentials), + grpc.KeepaliveParams(keepalive.ServerParameters{Time: 15 * time.Second}), + grpc.ChainStreamInterceptor( + grpcUserAPIMetrics.StreamServerInterceptor(), + ), + grpc.ChainUnaryInterceptor( + grpcUserAPIMetrics.UnaryServerInterceptor(), + ), + ) + s := &recoveryAPIServer{ + grpc: grpcServer, + logger: log.WithGroup("recoveryapi"), + recoverable: recoveryTarget, + recoveryDoneC: make(chan struct{}), + } + recoveryapi.RegisterRecoveryAPIServer(s.grpc, s) + + grpcUserAPIMetrics.InitializeMetrics(grpcServer) + reg.MustRegister(grpcUserAPIMetrics) + + return s +} + +func (s *recoveryAPIServer) Serve(endpoint string) error { + lis, err := net.Listen("tcp", endpoint) + if err != nil { + return fmt.Errorf("listening on %s: %w", endpoint, err) + } + return s.grpc.Serve(lis) +} + +func (s *recoveryAPIServer) WaitRecoveryDone() { + <-s.recoveryDoneC +} + +func (s *recoveryAPIServer) Recover(_ context.Context, req *recoveryapi.RecoverRequest) (*recoveryapi.RecoverResponse, error) { + return &recoveryapi.RecoverResponse{}, s.recoverable.Recover(req.Seed, req.Salt) +} + +type recoverable interface { + Recover(seed, salt []byte) error +} diff --git a/internal/recoveryapi/recoveryapi.go b/internal/recoveryapi/recoveryapi.go new file mode 100644 index 000000000..579805291 --- /dev/null +++ b/internal/recoveryapi/recoveryapi.go @@ -0,0 +1,9 @@ +// Copyright 2024 Edgeless Systems GmbH +// SPDX-License-Identifier: AGPL-3.0-only + +package recoveryapi + +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative recoveryapi.proto + +// Port is the port of the coordinator API. +const Port = "1314" diff --git a/internal/recoveryapi/recoveryapi.pb.go b/internal/recoveryapi/recoveryapi.pb.go new file mode 100644 index 000000000..b50971c55 --- /dev/null +++ b/internal/recoveryapi/recoveryapi.pb.go @@ -0,0 +1,214 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc v4.24.4 +// source: recoveryapi.proto + +package recoveryapi + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RecoverRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Seed []byte `protobuf:"bytes,1,opt,name=Seed,proto3" json:"Seed,omitempty"` + Salt []byte `protobuf:"bytes,2,opt,name=Salt,proto3" json:"Salt,omitempty"` +} + +func (x *RecoverRequest) Reset() { + *x = RecoverRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_recoveryapi_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RecoverRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecoverRequest) ProtoMessage() {} + +func (x *RecoverRequest) ProtoReflect() protoreflect.Message { + mi := &file_recoveryapi_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RecoverRequest.ProtoReflect.Descriptor instead. +func (*RecoverRequest) Descriptor() ([]byte, []int) { + return file_recoveryapi_proto_rawDescGZIP(), []int{0} +} + +func (x *RecoverRequest) GetSeed() []byte { + if x != nil { + return x.Seed + } + return nil +} + +func (x *RecoverRequest) GetSalt() []byte { + if x != nil { + return x.Salt + } + return nil +} + +type RecoverResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RecoverResponse) Reset() { + *x = RecoverResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_recoveryapi_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RecoverResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecoverResponse) ProtoMessage() {} + +func (x *RecoverResponse) ProtoReflect() protoreflect.Message { + mi := &file_recoveryapi_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RecoverResponse.ProtoReflect.Descriptor instead. +func (*RecoverResponse) Descriptor() ([]byte, []int) { + return file_recoveryapi_proto_rawDescGZIP(), []int{1} +} + +var File_recoveryapi_proto protoreflect.FileDescriptor + +var file_recoveryapi_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x61, 0x70, 0x69, + 0x22, 0x38, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x53, 0x65, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x61, 0x6c, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x53, 0x61, 0x6c, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x52, 0x65, + 0x63, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x53, 0x0a, + 0x0b, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x41, 0x50, 0x49, 0x12, 0x44, 0x0a, 0x07, + 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, + 0x72, 0x79, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x61, + 0x70, 0x69, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x61, 0x73, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x72, + 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_recoveryapi_proto_rawDescOnce sync.Once + file_recoveryapi_proto_rawDescData = file_recoveryapi_proto_rawDesc +) + +func file_recoveryapi_proto_rawDescGZIP() []byte { + file_recoveryapi_proto_rawDescOnce.Do(func() { + file_recoveryapi_proto_rawDescData = protoimpl.X.CompressGZIP(file_recoveryapi_proto_rawDescData) + }) + return file_recoveryapi_proto_rawDescData +} + +var file_recoveryapi_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_recoveryapi_proto_goTypes = []interface{}{ + (*RecoverRequest)(nil), // 0: recoveryapi.RecoverRequest + (*RecoverResponse)(nil), // 1: recoveryapi.RecoverResponse +} +var file_recoveryapi_proto_depIdxs = []int32{ + 0, // 0: recoveryapi.RecoveryAPI.Recover:input_type -> recoveryapi.RecoverRequest + 1, // 1: recoveryapi.RecoveryAPI.Recover:output_type -> recoveryapi.RecoverResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_recoveryapi_proto_init() } +func file_recoveryapi_proto_init() { + if File_recoveryapi_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_recoveryapi_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RecoverRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_recoveryapi_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RecoverResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_recoveryapi_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_recoveryapi_proto_goTypes, + DependencyIndexes: file_recoveryapi_proto_depIdxs, + MessageInfos: file_recoveryapi_proto_msgTypes, + }.Build() + File_recoveryapi_proto = out.File + file_recoveryapi_proto_rawDesc = nil + file_recoveryapi_proto_goTypes = nil + file_recoveryapi_proto_depIdxs = nil +} diff --git a/internal/recoveryapi/recoveryapi.proto b/internal/recoveryapi/recoveryapi.proto new file mode 100644 index 000000000..7026d9172 --- /dev/null +++ b/internal/recoveryapi/recoveryapi.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package recoveryapi; + +option go_package = "github.com/edgelesssys/contrast/internal/recoveryapi"; + +service RecoveryAPI { + rpc Recover(RecoverRequest) returns (RecoverResponse); +} + +message RecoverRequest { + bytes Seed = 1; + bytes Salt = 2; +} + +message RecoverResponse {} diff --git a/internal/recoveryapi/recoveryapi_grpc.pb.go b/internal/recoveryapi/recoveryapi_grpc.pb.go new file mode 100644 index 000000000..238937932 --- /dev/null +++ b/internal/recoveryapi/recoveryapi_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.24.4 +// source: recoveryapi.proto + +package recoveryapi + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + RecoveryAPI_Recover_FullMethodName = "/recoveryapi.RecoveryAPI/Recover" +) + +// RecoveryAPIClient is the client API for RecoveryAPI service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RecoveryAPIClient interface { + Recover(ctx context.Context, in *RecoverRequest, opts ...grpc.CallOption) (*RecoverResponse, error) +} + +type recoveryAPIClient struct { + cc grpc.ClientConnInterface +} + +func NewRecoveryAPIClient(cc grpc.ClientConnInterface) RecoveryAPIClient { + return &recoveryAPIClient{cc} +} + +func (c *recoveryAPIClient) Recover(ctx context.Context, in *RecoverRequest, opts ...grpc.CallOption) (*RecoverResponse, error) { + out := new(RecoverResponse) + err := c.cc.Invoke(ctx, RecoveryAPI_Recover_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RecoveryAPIServer is the server API for RecoveryAPI service. +// All implementations must embed UnimplementedRecoveryAPIServer +// for forward compatibility +type RecoveryAPIServer interface { + Recover(context.Context, *RecoverRequest) (*RecoverResponse, error) + mustEmbedUnimplementedRecoveryAPIServer() +} + +// UnimplementedRecoveryAPIServer must be embedded to have forward compatible implementations. +type UnimplementedRecoveryAPIServer struct { +} + +func (UnimplementedRecoveryAPIServer) Recover(context.Context, *RecoverRequest) (*RecoverResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Recover not implemented") +} +func (UnimplementedRecoveryAPIServer) mustEmbedUnimplementedRecoveryAPIServer() {} + +// UnsafeRecoveryAPIServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RecoveryAPIServer will +// result in compilation errors. +type UnsafeRecoveryAPIServer interface { + mustEmbedUnimplementedRecoveryAPIServer() +} + +func RegisterRecoveryAPIServer(s grpc.ServiceRegistrar, srv RecoveryAPIServer) { + s.RegisterService(&RecoveryAPI_ServiceDesc, srv) +} + +func _RecoveryAPI_Recover_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RecoverRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RecoveryAPIServer).Recover(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RecoveryAPI_Recover_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RecoveryAPIServer).Recover(ctx, req.(*RecoverRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RecoveryAPI_ServiceDesc is the grpc.ServiceDesc for RecoveryAPI service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RecoveryAPI_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "recoveryapi.RecoveryAPI", + HandlerType: (*RecoveryAPIServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Recover", + Handler: _RecoveryAPI_Recover_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "recoveryapi.proto", +}