refactor(enclave): migrate to enclave signing with MPC

This commit is contained in:
2026-01-10 16:49:23 -05:00
parent b6a01a07ae
commit 659736ade0
10 changed files with 283 additions and 93 deletions

View File

@@ -52,7 +52,7 @@ func ping() int32 {
//go:wasmexport generate //go:wasmexport generate
func generate() int32 { func generate() int32 {
pdk.Log(pdk.LogInfo, "generate: starting database initialization") pdk.Log(pdk.LogInfo, "generate: starting")
var input types.GenerateInput var input types.GenerateInput
if err := pdk.InputJSON(&input); err != nil { if err := pdk.InputJSON(&input); err != nil {
@@ -71,44 +71,32 @@ func generate() int32 {
return 1 return 1
} }
pdk.Log(pdk.LogInfo, "generate: opening keybase") result, err := initializeWithMPC(credentialBytes)
kb, err := keybase.Open()
if err != nil { if err != nil {
pdk.SetError(fmt.Errorf("generate: open database: %w", err)) pdk.SetError(fmt.Errorf("generate: %w", err))
return 1 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.SetInitialized(true)
state.SetDID(did) state.SetDID(result.DID)
pdk.Log(pdk.LogInfo, "generate: serializing database")
dbBytes, err := serializeDatabase() dbBytes, err := serializeDatabase()
if err != nil { if err != nil {
pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) pdk.SetError(fmt.Errorf("generate: serialize: %w", err))
return 1 return 1
} }
output := types.GenerateOutput{ output := types.GenerateOutput{
DID: did, DID: result.DID,
Database: dbBytes, Database: dbBytes,
} }
if err := pdk.OutputJSON(output); err != nil { 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 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 return 0
} }
@@ -262,28 +250,21 @@ type initResult struct {
} }
func initializeWithMPC(credentialBytes []byte) (*initResult, error) { func initializeWithMPC(credentialBytes []byte) (*initResult, error) {
pdk.Log(pdk.LogInfo, "initializeWithMPC: step 1 - opening database")
kb, err := keybase.Open() kb, err := keybase.Open()
if err != nil { if err != nil {
return nil, fmt.Errorf("open database: %w", err) return nil, fmt.Errorf("open database: %w", err)
} }
pdk.Log(pdk.LogInfo, "initializeWithMPC: step 2 - database opened")
ctx := context.Background() ctx := context.Background()
pdk.Log(pdk.LogInfo, "initializeWithMPC: step 3 - initializing DID")
did, err := kb.Initialize(ctx, credentialBytes) did, err := kb.Initialize(ctx, credentialBytes)
if err != nil { 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() simpleEnc, err := mpc.NewSimpleEnclave()
if err != nil { if err != nil {
pdk.Log(pdk.LogError, fmt.Sprintf("initializeWithMPC: enclave generation failed: %v", err))
return nil, fmt.Errorf("generate enclave: %w", 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]) 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) 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()) accounts, err := createDefaultAccounts(ctx, am, enc.ID, simpleEnc.PubKeyBytes())
if err != nil { 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{} 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) { 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) derivedAccounts, err := bip44.DeriveAccounts(pubKeyBytes, chains)
if err != nil { if err != nil {
return nil, fmt.Errorf("derive accounts: %w", err) return nil, fmt.Errorf("derive accounts: %w", err)
@@ -346,7 +325,6 @@ func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, encla
IsDefault: isDefault, IsDefault: isDefault,
}) })
if err != nil { if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: failed for %s: %s", derived.ChainID, err))
continue continue
} }
@@ -538,15 +516,9 @@ func matchResource(pattern, resource string) bool {
} }
func executeAction(params *types.FilterParams) (json.RawMessage, error) { func executeAction(params *types.FilterParams) (json.RawMessage, error) {
if params.Resource == "accounts" { if params.Resource == "accounts" && params.Action == "balances" {
switch params.Action {
case "balances":
return fetchAccountBalances(params.Subject) return fetchAccountBalances(params.Subject)
case "sign":
return json.Marshal(map[string]string{"signature": "placeholder"})
} }
}
return keybase.Exec(context.Background(), params.Resource, params.Action, params.Subject) return keybase.Exec(context.Background(), params.Resource, params.Action, params.Subject)
} }

View File

@@ -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
}

View File

@@ -2,28 +2,106 @@ package mpc
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic"
"fmt"
"math/big"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
) )
func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error) { func VerifyWithPubKey(pubKey []byte, data []byte, sig []byte) (bool, error) {
edSig, err := DeserializeSignature(sig) if len(sig) != 64 {
if err != nil { return false, fmt.Errorf("invalid signature length: expected 64, got %d", len(sig))
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,
} }
// 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 := sha3.New256()
hash.Write(data) hash.Write(data)
digest := hash.Sum(nil) 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
} }

View File

@@ -6,10 +6,10 @@ import (
"code.sonr.org/go/did-it" "code.sonr.org/go/did-it"
"code.sonr.org/go/did-it/crypto" "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/command"
"code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/pkg/policy"
"code.sonr.org/go/ucan/token/delegation" "code.sonr.org/go/ucan/token/delegation"
"github.com/ipfs/go-cid"
) )
// DelegationBuilder provides a fluent API for creating UCAN delegations. // DelegationBuilder provides a fluent API for creating UCAN delegations.

View File

@@ -6,9 +6,9 @@ import (
"code.sonr.org/go/did-it" "code.sonr.org/go/did-it"
"code.sonr.org/go/did-it/crypto" "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/command"
"code.sonr.org/go/ucan/token/invocation" "code.sonr.org/go/ucan/token/invocation"
"github.com/ipfs/go-cid"
) )
// InvocationBuilder provides a fluent API for creating UCAN invocations. // InvocationBuilder provides a fluent API for creating UCAN invocations.

View File

@@ -1,9 +1,9 @@
package ucan package ucan
import ( import (
"github.com/ipld/go-ipld-prime"
"code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/pkg/policy"
"code.sonr.org/go/ucan/pkg/policy/literal" "code.sonr.org/go/ucan/pkg/policy/literal"
"github.com/ipld/go-ipld-prime"
) )
// PolicyBuilder provides a fluent API for constructing UCAN policies. // PolicyBuilder provides a fluent API for constructing UCAN policies.

View File

@@ -1,8 +1,8 @@
package ucan package ucan
import ( import (
"github.com/ipfs/go-cid"
"code.sonr.org/go/ucan/pkg/policy" "code.sonr.org/go/ucan/pkg/policy"
"github.com/ipfs/go-cid"
) )
// ValidationErrorCode represents UCAN validation error types. // ValidationErrorCode represents UCAN validation error types.

View File

@@ -3,6 +3,8 @@ package keybase
import ( import (
"context" "context"
"fmt" "fmt"
"enclave/internal/crypto/mpc"
) )
type EnclaveResult struct { 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 { func enclaveToResult(enc *MpcEnclafe) *EnclaveResult {
rotatedAt := "" rotatedAt := ""
if enc.RotatedAt != nil { if enc.RotatedAt != nil {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings"
) )
type HandlerFunc func(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) type HandlerFunc func(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error)
@@ -15,6 +16,7 @@ var handlers = map[string]resourceHandlers{
"accounts": { "accounts": {
"list": handleAccountList, "list": handleAccountList,
"get": handleAccountGet, "get": handleAccountGet,
"sign": handleAccountSign,
}, },
"credentials": { "credentials": {
"list": handleCredentialList, "list": handleCredentialList,
@@ -31,6 +33,7 @@ var handlers = map[string]resourceHandlers{
"enclaves": { "enclaves": {
"list": handleEnclaveList, "list": handleEnclaveList,
"get": handleEnclaveGet, "get": handleEnclaveGet,
"sign": handleEnclaveSign,
"rotate": handleEnclaveRotate, "rotate": handleEnclaveRotate,
"archive": handleEnclaveArchive, "archive": handleEnclaveArchive,
"delete": handleEnclaveDelete, "delete": handleEnclaveDelete,
@@ -101,6 +104,32 @@ func handleAccountGet(ctx context.Context, am *ActionManager, subject string) (j
return json.Marshal(account) 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) { func handleCredentialList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
credentials, err := am.ListCredentials(ctx) credentials, err := am.ListCredentials(ctx)
if err != nil { if err != nil {
@@ -179,6 +208,37 @@ func handleEnclaveGet(ctx context.Context, am *ActionManager, subject string) (j
return json.Marshal(enc) 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) { func handleEnclaveRotate(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
if subject == "" { if subject == "" {
return nil, errors.New("subject (enclave_id) required for rotate action") return nil, errors.New("subject (enclave_id) required for rotate action")

View File

@@ -6,6 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"enclave/internal/crypto/bip44"
"enclave/internal/crypto/mpc" "enclave/internal/crypto/mpc"
"github.com/ncruces/go-sqlite3" "github.com/ncruces/go-sqlite3"
@@ -24,6 +25,12 @@ func RegisterMPCFunctions(conn *sqlite3.Conn) error {
if err := registerRefreshFunction(conn); err != nil { if err := registerRefreshFunction(conn); err != nil {
return fmt.Errorf("register mpc_refresh: %w", err) 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 return nil
} }
@@ -221,3 +228,84 @@ func updateSimpleEnclaveInDB(enclaveID string, enclave *mpc.SimpleEnclave) error
return err 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)
})
}