Skip to content

Commit ca5f25f

Browse files
cboh4vlad
authored andcommitted
feat: add billing auth interceptor for non-SGX nodes
1 parent 1c6ccaa commit ca5f25f

3 files changed

Lines changed: 104 additions & 19 deletions

File tree

go-cosmwasm/api/ecall_client.go

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,36 @@ package api
55

66
import (
77
"context"
8+
"crypto/sha256"
9+
"encoding/hex"
810
"encoding/json"
911
"fmt"
1012
"math/rand"
1113
"os"
1214
"path/filepath"
15+
"strconv"
16+
"strings"
1317
"sync"
1418
"time"
1519

20+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
21+
dcrdecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
1622
"github.com/gogo/protobuf/proto"
1723
"google.golang.org/grpc"
1824
"google.golang.org/grpc/codes"
1925
"google.golang.org/grpc/credentials/insecure"
26+
"google.golang.org/grpc/metadata"
2027
"google.golang.org/grpc/status"
2128
)
2229

2330
// EcallClient fetches ecall records from remote SGX nodes via gRPC
2431
// It maintains connections to multiple nodes and selects randomly for load distribution
2532
type EcallClient struct {
26-
mu sync.RWMutex
27-
nodes []*nodeConn // Pool of node connections
28-
timeout time.Duration
29-
rng *rand.Rand
33+
mu sync.RWMutex
34+
nodes []*nodeConn // Pool of node connections
35+
timeout time.Duration
36+
rng *rand.Rand
37+
billingPrivKey *secp256k1.PrivateKey // loaded from hex file for billing sidecar auth
3038
}
3139

3240
// nodeConn represents a connection to a single SGX node
@@ -324,10 +332,37 @@ func GetEcallClient() *EcallClient {
324332
nodes[i] = &nodeConn{addr: addr}
325333
}
326334

335+
// Load billing key from hex file
336+
var billingPrivKey *secp256k1.PrivateKey
337+
keyFile := os.Getenv("SECRET_BILLING_KEY_FILE")
338+
if keyFile == "" {
339+
homeDir := os.Getenv("HOME")
340+
defaultPath := filepath.Join(homeDir, ".secretd-billing", "key.hex")
341+
if _, err := os.Stat(defaultPath); err == nil {
342+
keyFile = defaultPath
343+
}
344+
}
345+
if keyFile != "" {
346+
data, err := os.ReadFile(keyFile)
347+
if err != nil {
348+
logWarn("EcallClient", "Failed to read billing key file %s: %v", keyFile, err)
349+
} else {
350+
keyHex := strings.TrimSpace(string(data))
351+
keyBytes, err := hex.DecodeString(keyHex)
352+
if err != nil {
353+
logWarn("EcallClient", "Invalid hex in billing key file %s: %v", keyFile, err)
354+
} else {
355+
billingPrivKey = secp256k1.PrivKeyFromBytes(keyBytes)
356+
logInfo("EcallClient", "Loaded billing key from %s", keyFile)
357+
}
358+
}
359+
}
360+
327361
globalClient = &EcallClient{
328-
nodes: nodes,
329-
timeout: 30 * time.Second,
330-
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
362+
nodes: nodes,
363+
timeout: 30 * time.Second,
364+
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
365+
billingPrivKey: billingPrivKey,
331366
}
332367

333368
// logInfo("EcallClient", "Initialized with %d SGX nodes", len(addrs))
@@ -421,11 +456,17 @@ func (c *EcallClient) ensureConnection(node *nodeConn) (*grpc.ClientConn, error)
421456
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
422457
defer cancel()
423458

459+
dialOpts := []grpc.DialOption{
460+
grpc.WithTransportCredentials(insecure.NewCredentials()),
461+
}
462+
if c.billingPrivKey != nil {
463+
dialOpts = append(dialOpts, grpc.WithUnaryInterceptor(c.billingAuthInterceptor()))
464+
}
465+
424466
conn, err := grpc.DialContext(
425467
ctx,
426468
node.addr,
427-
grpc.WithTransportCredentials(insecure.NewCredentials()),
428-
grpc.WithBlock(),
469+
dialOpts...,
429470
)
430471
if err != nil {
431472
node.failed = true
@@ -688,3 +729,47 @@ func (c *EcallClient) FetchNetworkPubkey(height int64, iSeed uint32) ([]byte, []
688729
// logInfo("EcallClient", "Fetched NetworkPubkey for height %d seed %d", height, iSeed)
689730
return resp.NodePubkey, resp.IoPubkey, nil
690731
}
732+
733+
// billingAuthInterceptor automatically signs the request payload using the loaded private key.
734+
// The signature allows the billing sidecar to authenticate the client and debit their subscription balance.
735+
func (c *EcallClient) billingAuthInterceptor() grpc.UnaryClientInterceptor {
736+
return func(
737+
ctx context.Context,
738+
method string,
739+
req, reply interface{},
740+
cc *grpc.ClientConn,
741+
invoker grpc.UnaryInvoker,
742+
opts ...grpc.CallOption,
743+
) error {
744+
if c.billingPrivKey != nil {
745+
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
746+
payload := timestamp + "|" + method
747+
hash := sha256.Sum256([]byte(payload))
748+
749+
// Sign with secp256k1
750+
sig := dcrdecdsa.Sign(c.billingPrivKey, hash[:])
751+
752+
// 64-byte compact R || S format
753+
var sigBytes [64]byte
754+
r := sig.R()
755+
s := sig.S()
756+
rBytes := r.Bytes()
757+
sBytes := s.Bytes()
758+
copy(sigBytes[0:32], rBytes[:])
759+
copy(sigBytes[32:64], sBytes[:])
760+
761+
// 33-byte compressed pubkey
762+
pubKey := c.billingPrivKey.PubKey()
763+
pubKeyBytes := pubKey.SerializeCompressed()
764+
765+
md := metadata.Pairs(
766+
"x-sub-timestamp", timestamp,
767+
"x-sub-pubkey", hex.EncodeToString(pubKeyBytes),
768+
"x-sub-signature", hex.EncodeToString(sigBytes[:]),
769+
)
770+
ctx = metadata.NewOutgoingContext(ctx, md)
771+
}
772+
773+
return invoker(ctx, method, req, reply, cc, opts...)
774+
}
775+
}

go-cosmwasm/api/lib_nosgx.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ func GetEncryptedSeed(cert []byte, replace_machine_id []byte) ([]byte, []byte, e
330330
outp1, outp2, err := client.FetchEncryptedSeed(height, certHashHex)
331331
if err == nil {
332332
//logInfo("GetEncryptedSeed", "Fetched seed from SGX node (attempt %d) for %s (%d bytes)",
333-
attempt+1, certHashHex, len(outp1))
333+
// attempt+1, certHashHex, len(outp1))
334334
// Cache locally
335335
if cacheErr := recorder.RecordGetEncryptedSeed(height, certHash[:], outp1, outp2); cacheErr != nil {
336336
logError("GetEncryptedSeed", "Failed to cache: %v", cacheErr)
@@ -339,22 +339,22 @@ func GetEncryptedSeed(cert []byte, replace_machine_id []byte) ([]byte, []byte, e
339339
}
340340

341341
// Extract gRPC status for logging
342-
grpcCode := "unknown"
343-
grpcMsg := err.Error()
344-
if st, ok := status.FromError(err); ok {
345-
grpcCode = st.Code().String()
346-
grpcMsg = st.Message()
347-
}
342+
//grpcCode := "unknown"
343+
//grpcMsg := err.Error()
344+
//if st, ok := status.FromError(err); ok {
345+
// grpcCode = st.Code().String()
346+
// grpcMsg = st.Message()
347+
//}
348348

349349
//logInfo("GetEncryptedSeed", "gRPC attempt %d/%d for %s: code=%s msg=%s",
350-
attempt+1, maxRetries, certHashHex, grpcCode, grpcMsg)
350+
// attempt+1, maxRetries, certHashHex, grpcCode, grpcMsg)
351351

352352
// Check if this is a FailedPrecondition error (recorded error from SGX node)
353353
if st, ok := status.FromError(err); ok && st.Code() == codes.FailedPrecondition {
354354
// The SGX node recorded the enclave error - cache and replay it
355355
enclaveErrMsg := st.Message()
356356
//logInfo("GetEncryptedSeed", "SGX node returned FailedPrecondition (enclave error) for %s: %s",
357-
certHashHex, enclaveErrMsg)
357+
// certHashHex, enclaveErrMsg)
358358
if cacheErr := recorder.RecordGetEncryptedSeedError(height, certHash[:], enclaveErrMsg); cacheErr != nil {
359359
logError("GetEncryptedSeed", "Failed to cache error: %v", cacheErr)
360360
}

x/compute/module.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func (am AppModule) BeginBlock(c context.Context) error {
208208
if err == nil {
209209
break
210210
}
211-
ctx.Logger().Info("Waiting for SGX node ecall record, retrying...", "height", height, "error", err)
211+
ctx.Logger().Debug("Waiting for SGX node ecall record, retrying...", "height", height, "error", err)
212212
time.Sleep(retryInterval)
213213
}
214214
random = record.RandomSeed

0 commit comments

Comments
 (0)