@@ -5,28 +5,36 @@ package api
55
66import (
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
2532type 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+ }
0 commit comments