From 659736ade089912a06d493de485f1f89a058ec16 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 10 Jan 2026 16:49:23 -0500 Subject: [PATCH] refactor(enclave): migrate to enclave signing with MPC --- cmd/enclave/main.go | 54 ++++---------- internal/crypto/mpc/utils.go | 33 --------- internal/crypto/mpc/verify.go | 108 ++++++++++++++++++++++++---- internal/crypto/ucan/delegation.go | 2 +- internal/crypto/ucan/invocation.go | 2 +- internal/crypto/ucan/policy.go | 2 +- internal/crypto/ucan/types.go | 2 +- internal/keybase/actions_enclave.go | 25 +++++++ internal/keybase/exec.go | 60 ++++++++++++++++ internal/keybase/functions.go | 88 +++++++++++++++++++++++ 10 files changed, 283 insertions(+), 93 deletions(-) delete mode 100644 internal/crypto/mpc/utils.go diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go index 4d8bf74..6b53ed2 100644 --- a/cmd/enclave/main.go +++ b/cmd/enclave/main.go @@ -52,7 +52,7 @@ func ping() int32 { //go:wasmexport generate func generate() int32 { - pdk.Log(pdk.LogInfo, "generate: starting database initialization") + pdk.Log(pdk.LogInfo, "generate: starting") var input types.GenerateInput if err := pdk.InputJSON(&input); err != nil { @@ -71,44 +71,32 @@ func generate() int32 { return 1 } - pdk.Log(pdk.LogInfo, "generate: opening keybase") - kb, err := keybase.Open() + result, err := initializeWithMPC(credentialBytes) if err != nil { - pdk.SetError(fmt.Errorf("generate: open database: %w", err)) + pdk.SetError(fmt.Errorf("generate: %w", err)) return 1 } - pdk.Log(pdk.LogInfo, "generate: initializing DID") - ctx := context.Background() - did, err := kb.Initialize(ctx, credentialBytes) - if err != nil { - pdk.SetError(fmt.Errorf("generate: initialize DID: %w", err)) - return 1 - } - - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: DID created: %s", did)) - state.SetInitialized(true) - state.SetDID(did) + state.SetDID(result.DID) - pdk.Log(pdk.LogInfo, "generate: serializing database") dbBytes, err := serializeDatabase() if err != nil { - pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) + pdk.SetError(fmt.Errorf("generate: serialize: %w", err)) return 1 } output := types.GenerateOutput{ - DID: did, + DID: result.DID, Database: dbBytes, } if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err)) + pdk.SetError(fmt.Errorf("generate: output: %w", err)) return 1 } - pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s (no MPC)", did)) + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s with enclave %s", result.DID, result.EnclaveID)) return 0 } @@ -262,28 +250,21 @@ type initResult struct { } func initializeWithMPC(credentialBytes []byte) (*initResult, error) { - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 1 - opening database") kb, err := keybase.Open() if err != nil { return nil, fmt.Errorf("open database: %w", err) } - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 2 - database opened") ctx := context.Background() - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 3 - initializing DID") did, err := kb.Initialize(ctx, credentialBytes) if err != nil { - return nil, fmt.Errorf("initialize: %w", err) + return nil, fmt.Errorf("initialize DID: %w", err) } - pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: step 4 - DID initialized: %s", did)) - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 5 - generating simple enclave") simpleEnc, err := mpc.NewSimpleEnclave() if err != nil { - pdk.Log(pdk.LogError, fmt.Sprintf("initializeWithMPC: enclave generation failed: %v", err)) return nil, fmt.Errorf("generate enclave: %w", err) } - pdk.Log(pdk.LogInfo, "initializeWithMPC: step 6 - enclave generated") enclaveID := fmt.Sprintf("enc_%x", credentialBytes[:8]) @@ -305,11 +286,9 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { return nil, fmt.Errorf("store enclave: %w", err) } - pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeWithMPC: stored enclave %s", enclaveID)) - accounts, err := createDefaultAccounts(ctx, am, enc.ID, simpleEnc.PubKeyBytes()) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeWithMPC: failed to create accounts: %s", err)) + pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: %s", err)) accounts = []types.AccountInfo{} } @@ -322,7 +301,7 @@ func initializeWithMPC(credentialBytes []byte) (*initResult, error) { } func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, enclaveID int64, pubKeyBytes []byte) ([]types.AccountInfo, error) { - chains := []string{"bitcoin", "ethereum", "sonr"} + chains := []string{"sonr", "ethereum", "bitcoin"} derivedAccounts, err := bip44.DeriveAccounts(pubKeyBytes, chains) if err != nil { return nil, fmt.Errorf("derive accounts: %w", err) @@ -346,7 +325,6 @@ func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, encla IsDefault: isDefault, }) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: failed for %s: %s", derived.ChainID, err)) continue } @@ -538,15 +516,9 @@ func matchResource(pattern, resource string) bool { } func executeAction(params *types.FilterParams) (json.RawMessage, error) { - if params.Resource == "accounts" { - switch params.Action { - case "balances": - return fetchAccountBalances(params.Subject) - case "sign": - return json.Marshal(map[string]string{"signature": "placeholder"}) - } + if params.Resource == "accounts" && params.Action == "balances" { + return fetchAccountBalances(params.Subject) } - return keybase.Exec(context.Background(), params.Resource, params.Action, params.Subject) } diff --git a/internal/crypto/mpc/utils.go b/internal/crypto/mpc/utils.go deleted file mode 100644 index b0407b6..0000000 --- a/internal/crypto/mpc/utils.go +++ /dev/null @@ -1,33 +0,0 @@ -package mpc - -import ( - "fmt" - "math/big" - - "github.com/sonr-io/crypto/core/curves" -) - -func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error) { - crv := curves.K256() - x := new(big.Int).SetBytes(pubKey[1:33]) - y := new(big.Int).SetBytes(pubKey[33:]) - ecCurve, err := crv.ToEllipticCurve() - if err != nil { - return nil, fmt.Errorf("error converting curve: %v", err) - } - return &curves.EcPoint{X: x, Y: y, Curve: ecCurve}, nil -} - -func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error) { - if len(sigBytes) != 64 { - return nil, fmt.Errorf("invalid signature length: expected 64 bytes, got %d", len(sigBytes)) - } - - r := new(big.Int).SetBytes(sigBytes[:32]) - s := new(big.Int).SetBytes(sigBytes[32:]) - - return &curves.EcdsaSignature{ - R: r, - S: s, - }, nil -} diff --git a/internal/crypto/mpc/verify.go b/internal/crypto/mpc/verify.go index 4163759..a4e3a45 100644 --- a/internal/crypto/mpc/verify.go +++ b/internal/crypto/mpc/verify.go @@ -2,28 +2,106 @@ package mpc import ( "crypto/ecdsa" + "crypto/elliptic" + "fmt" + "math/big" "golang.org/x/crypto/sha3" ) -func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error) { - edSig, err := DeserializeSignature(sig) - if err != nil { - return false, err - } - ePub, err := GetECDSAPoint(pubKeyCompressed) - if err != nil { - return false, err - } - pk := &ecdsa.PublicKey{ - Curve: ePub.Curve, - X: ePub.X, - Y: ePub.Y, +func VerifyWithPubKey(pubKey []byte, data []byte, sig []byte) (bool, error) { + if len(sig) != 64 { + return false, fmt.Errorf("invalid signature length: expected 64, got %d", len(sig)) } - // Hash the message using SHA3-256 + pk, err := parsePublicKey(pubKey) + if err != nil { + return false, err + } + + r := new(big.Int).SetBytes(sig[:32]) + s := new(big.Int).SetBytes(sig[32:]) + hash := sha3.New256() hash.Write(data) digest := hash.Sum(nil) - return ecdsa.Verify(pk, digest, edSig.R, edSig.S), nil + + return ecdsa.Verify(pk, digest, r, s), nil +} + +func parsePublicKey(pubKey []byte) (*ecdsa.PublicKey, error) { + curve := elliptic.P256() + // Use secp256k1 parameters manually since Go stdlib doesn't include it + curve = secp256k1Curve() + + switch len(pubKey) { + case 65: // uncompressed: 0x04 || x || y + if pubKey[0] != 0x04 { + return nil, fmt.Errorf("invalid uncompressed pubkey prefix: %x", pubKey[0]) + } + x := new(big.Int).SetBytes(pubKey[1:33]) + y := new(big.Int).SetBytes(pubKey[33:65]) + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil + + case 33: // compressed: 0x02/0x03 || x + x, y := decompressPoint(curve, pubKey) + if x == nil { + return nil, fmt.Errorf("failed to decompress pubkey") + } + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil + + default: + return nil, fmt.Errorf("invalid pubkey length: %d", len(pubKey)) + } +} + +func secp256k1Curve() elliptic.Curve { + return &secp256k1Params +} + +var secp256k1Params = elliptic.CurveParams{ + Name: "secp256k1", + BitSize: 256, + P: fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"), + N: fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"), + B: big.NewInt(7), + Gx: fromHex("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"), + Gy: fromHex("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"), +} + +func fromHex(s string) *big.Int { + i, _ := new(big.Int).SetString(s, 16) + return i +} + +func decompressPoint(curve elliptic.Curve, compressed []byte) (*big.Int, *big.Int) { + if len(compressed) != 33 { + return nil, nil + } + + prefix := compressed[0] + if prefix != 0x02 && prefix != 0x03 { + return nil, nil + } + + x := new(big.Int).SetBytes(compressed[1:]) + p := curve.Params().P + + // y² = x³ + 7 (for secp256k1) + x3 := new(big.Int).Mul(x, x) + x3.Mul(x3, x) + x3.Add(x3, big.NewInt(7)) + x3.Mod(x3, p) + + y := new(big.Int).ModSqrt(x3, p) + if y == nil { + return nil, nil + } + + // Check parity + if (y.Bit(0) == 1) != (prefix == 0x03) { + y.Sub(p, y) + } + + return x, y } diff --git a/internal/crypto/ucan/delegation.go b/internal/crypto/ucan/delegation.go index 7b42050..0857d20 100644 --- a/internal/crypto/ucan/delegation.go +++ b/internal/crypto/ucan/delegation.go @@ -6,10 +6,10 @@ import ( "code.sonr.org/go/did-it" "code.sonr.org/go/did-it/crypto" - "github.com/ipfs/go-cid" "code.sonr.org/go/ucan/pkg/command" "code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/token/delegation" + "github.com/ipfs/go-cid" ) // DelegationBuilder provides a fluent API for creating UCAN delegations. diff --git a/internal/crypto/ucan/invocation.go b/internal/crypto/ucan/invocation.go index d925fd6..db17d0e 100644 --- a/internal/crypto/ucan/invocation.go +++ b/internal/crypto/ucan/invocation.go @@ -6,9 +6,9 @@ import ( "code.sonr.org/go/did-it" "code.sonr.org/go/did-it/crypto" - "github.com/ipfs/go-cid" "code.sonr.org/go/ucan/pkg/command" "code.sonr.org/go/ucan/token/invocation" + "github.com/ipfs/go-cid" ) // InvocationBuilder provides a fluent API for creating UCAN invocations. diff --git a/internal/crypto/ucan/policy.go b/internal/crypto/ucan/policy.go index e8ed4d9..0873516 100644 --- a/internal/crypto/ucan/policy.go +++ b/internal/crypto/ucan/policy.go @@ -1,9 +1,9 @@ package ucan import ( - "github.com/ipld/go-ipld-prime" "code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/pkg/policy/literal" + "github.com/ipld/go-ipld-prime" ) // PolicyBuilder provides a fluent API for constructing UCAN policies. diff --git a/internal/crypto/ucan/types.go b/internal/crypto/ucan/types.go index 2589309..afb500e 100644 --- a/internal/crypto/ucan/types.go +++ b/internal/crypto/ucan/types.go @@ -1,8 +1,8 @@ package ucan import ( - "github.com/ipfs/go-cid" "code.sonr.org/go/ucan/pkg/policy" + "github.com/ipfs/go-cid" ) // ValidationErrorCode represents UCAN validation error types. diff --git a/internal/keybase/actions_enclave.go b/internal/keybase/actions_enclave.go index 088481c..e6f3cd0 100644 --- a/internal/keybase/actions_enclave.go +++ b/internal/keybase/actions_enclave.go @@ -3,6 +3,8 @@ package keybase import ( "context" "fmt" + + "enclave/internal/crypto/mpc" ) type EnclaveResult struct { @@ -126,6 +128,29 @@ func (am *ActionManager) DeleteEnclave(ctx context.Context, enclaveID string) er }) } +func (am *ActionManager) SignWithEnclave(ctx context.Context, enclaveID string, data []byte) ([]byte, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID) + if err != nil { + return nil, fmt.Errorf("get enclave: %w", err) + } + + simpleEnc, err := mpc.ImportSimpleEnclave( + enc.PublicKey, + enc.ValShare, + enc.UserShare, + enc.Nonce, + mpc.CurveName(enc.Curve), + ) + if err != nil { + return nil, fmt.Errorf("import enclave: %w", err) + } + + return simpleEnc.Sign(data) +} + func enclaveToResult(enc *MpcEnclafe) *EnclaveResult { rotatedAt := "" if enc.RotatedAt != nil { diff --git a/internal/keybase/exec.go b/internal/keybase/exec.go index f30fd8b..72db39b 100644 --- a/internal/keybase/exec.go +++ b/internal/keybase/exec.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" ) type HandlerFunc func(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) @@ -15,6 +16,7 @@ var handlers = map[string]resourceHandlers{ "accounts": { "list": handleAccountList, "get": handleAccountGet, + "sign": handleAccountSign, }, "credentials": { "list": handleCredentialList, @@ -31,6 +33,7 @@ var handlers = map[string]resourceHandlers{ "enclaves": { "list": handleEnclaveList, "get": handleEnclaveGet, + "sign": handleEnclaveSign, "rotate": handleEnclaveRotate, "archive": handleEnclaveArchive, "delete": handleEnclaveDelete, @@ -101,6 +104,32 @@ func handleAccountGet(ctx context.Context, am *ActionManager, subject string) (j return json.Marshal(account) } +func handleAccountSign(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (hex-encoded data) required for sign action") + } + + enclaves, err := am.ListEnclaves(ctx) + if err != nil { + return nil, fmt.Errorf("list enclaves: %w", err) + } + if len(enclaves) == 0 { + return nil, errors.New("no enclave available for signing") + } + + enc := enclaves[0] + signature, err := am.SignWithEnclave(ctx, enc.EnclaveID, []byte(subject)) + if err != nil { + return nil, fmt.Errorf("sign: %w", err) + } + + return json.Marshal(map[string]string{ + "signature": fmt.Sprintf("%x", signature), + "enclave_id": enc.EnclaveID, + "public_key": enc.PublicKeyHex, + }) +} + func handleCredentialList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) { credentials, err := am.ListCredentials(ctx) if err != nil { @@ -179,6 +208,37 @@ func handleEnclaveGet(ctx context.Context, am *ActionManager, subject string) (j return json.Marshal(enc) } +func handleEnclaveSign(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { + if subject == "" { + return nil, errors.New("subject (enclave_id:hex_data) required for sign action") + } + + parts := strings.SplitN(subject, ":", 2) + if len(parts) != 2 { + return nil, errors.New("subject must be enclave_id:hex_data format") + } + + enclaveID := parts[0] + data := []byte(parts[1]) + + signature, err := am.SignWithEnclave(ctx, enclaveID, data) + if err != nil { + return nil, err + } + + enc, _ := am.GetEnclaveByID(ctx, enclaveID) + pubKeyHex := "" + if enc != nil { + pubKeyHex = enc.PublicKeyHex + } + + return json.Marshal(map[string]string{ + "signature": fmt.Sprintf("%x", signature), + "enclave_id": enclaveID, + "public_key": pubKeyHex, + }) +} + func handleEnclaveRotate(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) { if subject == "" { return nil, errors.New("subject (enclave_id) required for rotate action") diff --git a/internal/keybase/functions.go b/internal/keybase/functions.go index becaa16..053833e 100644 --- a/internal/keybase/functions.go +++ b/internal/keybase/functions.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" + "enclave/internal/crypto/bip44" "enclave/internal/crypto/mpc" "github.com/ncruces/go-sqlite3" @@ -24,6 +25,12 @@ func RegisterMPCFunctions(conn *sqlite3.Conn) error { if err := registerRefreshFunction(conn); err != nil { return fmt.Errorf("register mpc_refresh: %w", err) } + if err := registerBIP44DeriveFunction(conn); err != nil { + return fmt.Errorf("register bip44_derive: %w", err) + } + if err := registerBIP44DeriveFromEnclaveFunction(conn); err != nil { + return fmt.Errorf("register bip44_derive_from_enclave: %w", err) + } return nil } @@ -221,3 +228,84 @@ func updateSimpleEnclaveInDB(enclaveID string, enclave *mpc.SimpleEnclave) error return err } + +// bip44_derive(pubkey_hex, chain) -> address +// Derives a blockchain address from a public key for the specified chain. +// Supported chains: bitcoin, ethereum, cosmos, sonr +func registerBIP44DeriveFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("bip44_derive", 2, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 2 { + ctx.ResultError(fmt.Errorf("bip44_derive requires 2 arguments: pubkey_hex, chain")) + return + } + + pubKeyHex := args[0].Text() + chain := args[1].Text() + + if pubKeyHex == "" { + ctx.ResultError(fmt.Errorf("bip44_derive: pubkey_hex cannot be empty")) + return + } + if chain == "" { + ctx.ResultError(fmt.Errorf("bip44_derive: chain cannot be empty")) + return + } + + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive: invalid pubkey hex: %w", err)) + return + } + + address, err := bip44.DeriveAddress(pubKeyBytes, chain) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive: %w", err)) + return + } + + ctx.ResultText(address) + }) +} + +// bip44_derive_from_enclave(enclave_id, chain) -> address +// Derives a blockchain address from an MPC enclave's public key. +func registerBIP44DeriveFromEnclaveFunction(conn *sqlite3.Conn) error { + return conn.CreateFunction("bip44_derive_from_enclave", 2, sqlite3.DETERMINISTIC, func(ctx sqlite3.Context, args ...sqlite3.Value) { + if len(args) != 2 { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave requires 2 arguments: enclave_id, chain")) + return + } + + enclaveID := args[0].Text() + chain := args[1].Text() + + if enclaveID == "" { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: enclave_id cannot be empty")) + return + } + if chain == "" { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: chain cannot be empty")) + return + } + + kb := Get() + if kb == nil { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: keybase not initialized")) + return + } + + dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: enclave not found: %w", err)) + return + } + + address, err := bip44.DeriveAddress(dbEnc.PublicKey, chain) + if err != nil { + ctx.ResultError(fmt.Errorf("bip44_derive_from_enclave: %w", err)) + return + } + + ctx.ResultText(address) + }) +}