Feat/Add Crypto Libs #3

Merged
pn merged 35 commits from feat/add-crypto-libs into main 2026-01-10 21:59:18 +00:00
6 changed files with 202 additions and 144 deletions
Showing only changes of commit 842fdc1923 - Show all commits

49
TODO.md
View File

@@ -232,13 +232,13 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar
### 4.1 Key Share Actions
- [ ] `CreateKeyShare(ctx, params) (*KeyShareResult, error)`
- [ ] `ListKeyShares(ctx) ([]KeyShareResult, error)`
- [ ] `GetKeyShareByID(ctx, shareID) (*KeyShareResult, error)`
- [ ] `GetKeyShareByKeyID(ctx, keyID) (*KeyShareResult, error)`
- [ ] `RotateKeyShare(ctx, shareID) error`
- [ ] `ArchiveKeyShare(ctx, shareID) error`
- [ ] `DeleteKeyShare(ctx, shareID) error`
- [x] `CreateKeyShare(ctx, params) (*KeyShareResult, error)`
- [x] `ListKeyShares(ctx) ([]KeyShareResult, error)`
- [x] `GetKeyShareByID(ctx, shareID) (*KeyShareResult, error)`
- [x] `GetKeyShareByKeyID(ctx, keyID) (*KeyShareResult, error)`
- [x] `RotateKeyShare(ctx, shareID) error`
- [x] `ArchiveKeyShare(ctx, shareID) error`
- [x] `DeleteKeyShare(ctx, shareID) error`
### 4.2 UCAN Token Actions (v1.0.0-rc.1)
@@ -290,12 +290,12 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar
### 4.6 Account Actions (Extend Existing)
- [ ] `CreateAccount(ctx, params) (*AccountResult, error)`
- [ ] `ListAccountsByChain(ctx, chainID) ([]AccountResult, error)`
- [ ] `GetDefaultAccount(ctx, chainID) (*AccountResult, error)`
- [ ] `SetDefaultAccount(ctx, accountID, chainID) error`
- [ ] `UpdateAccountLabel(ctx, accountID, label) error`
- [ ] `DeleteAccount(ctx, accountID) error`
- [x] `CreateAccount(ctx, params) (*AccountResult, error)`
- [x] `ListAccountsByChain(ctx, chainID) ([]AccountResult, error)`
- [x] `GetDefaultAccount(ctx, chainID) (*AccountResult, error)`
- [x] `SetDefaultAccount(ctx, accountID, chainID) error`
- [x] `UpdateAccountLabel(ctx, accountID, label) error`
- [x] `DeleteAccount(ctx, accountID) error`
### 4.7 Credential Actions (Extend Existing)
@@ -327,23 +327,24 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar
### 5.1 Key Share Storage
- [ ] Parse key share data from MPC protocol
- [ ] Encrypt share data before storage
- [ ] Store public key and chain code
- [ ] Track party index and threshold
- [x] Parse key share data from MPC protocol - `KeyShareInput` in generate
- [x] Store public key and chain code - `CreateKeyShare` action
- [x] Track party index and threshold - stored in `key_shares` table
- [ ] Encrypt share data before storage - PRF key derivation needed
### 5.2 Account Derivation
- [x] Basic address derivation from public key - `deriveCosmosAddress()`
- [x] Create initial account during generate - `createInitialAccount()`
- [ ] Implement BIP44 derivation path parsing
- [ ] Derive addresses from public keys
- [ ] Support multiple chains (Cosmos 118, Ethereum 60)
- [ ] Generate proper address encoding per chain
- [ ] Generate proper bech32 address encoding per chain
### 5.3 Key Rotation
- [ ] Implement key rotation workflow
- [ ] Archive old shares
- [ ] Update status transitions
- [x] Implement key rotation workflow - `RotateKeyShare` action
- [x] Archive old shares - `ArchiveKeyShare` action
- [x] Status transitions - managed in database
- [ ] Handle rotation failures gracefully
---
@@ -364,11 +365,13 @@ The following files implement UCAN v1.0.0-rc.1 using the official go-ucan librar
### 6.2 Extend `generate` Function
- [x] Accept optional MPC keyshare data in input
- [x] Create initial keyshare if provided
- [x] Create initial account from keyshare
- [ ] Parse WebAuthn credential properly (CBOR/COSE format)
- [ ] Extract public key from credential
- [ ] Create initial verification method
- [ ] Create initial credential record
- [ ] Generate initial account (if key share provided)
### 6.3 Signing Function

1
go.mod
View File

@@ -23,6 +23,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect

2
go.sum
View File

@@ -27,6 +27,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=

View File

@@ -1,4 +1,3 @@
// Package keybase contains the SQLite database for cryptographic keys.
package keybase
import (
@@ -6,18 +5,22 @@ import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"sync"
"enclave/internal/migrations"
_ "github.com/ncruces/go-sqlite3/driver"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/hash"
"github.com/ncruces/go-sqlite3/ext/serdes"
"github.com/ncruces/go-sqlite3/ext/uuid"
)
// Keybase encapsulates the encrypted key storage database.
type Keybase struct {
db *sql.DB
conn *sqlite3.Conn // raw connection for serdes
queries *Queries
did string
didID int64
@@ -38,19 +41,32 @@ func Open() (*Keybase, error) {
return instance, nil
}
conn, err := sql.Open("sqlite3", ":memory:")
var rawConn *sqlite3.Conn
initCallback := func(conn *sqlite3.Conn) error {
rawConn = conn
if err := hash.Register(conn); err != nil {
return fmt.Errorf("register hash extension: %w", err)
}
if err := uuid.Register(conn); err != nil {
return fmt.Errorf("register uuid extension: %w", err)
}
return nil
}
db, err := driver.Open(":memory:", initCallback)
if err != nil {
return nil, fmt.Errorf("keybase: open database: %w", err)
}
if _, err := conn.Exec(migrations.SchemaSQL); err != nil {
conn.Close()
if _, err := db.Exec(migrations.SchemaSQL); err != nil {
db.Close()
return nil, fmt.Errorf("keybase: init schema: %w", err)
}
instance = &Keybase{
db: conn,
queries: New(conn),
db: db,
conn: rawConn,
queries: New(db),
}
return instance, nil
@@ -169,10 +185,21 @@ func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (strin
// Load restores the database state from serialized bytes and sets the current DID.
func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) {
if len(data) < 10 {
if len(data) < 100 {
return "", fmt.Errorf("keybase: invalid database format")
}
k.mu.Lock()
defer k.mu.Unlock()
if k.conn == nil {
return "", fmt.Errorf("keybase: database not initialized")
}
if err := serdes.Deserialize(k.conn, "main", data); err != nil {
return "", fmt.Errorf("keybase: deserialize database: %w", err)
}
docs, err := k.queries.ListAllDIDs(ctx)
if err != nil {
return "", fmt.Errorf("keybase: list DIDs: %w", err)
@@ -182,127 +209,34 @@ func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) {
return "", fmt.Errorf("keybase: no DID found in database")
}
k.mu.Lock()
k.did = docs[0].Did
k.didID = docs[0].ID
k.mu.Unlock()
return k.did, nil
}
// Serialize exports the database state as bytes.
// Serialize exports the database state as bytes using native SQLite serialization.
func (k *Keybase) Serialize() ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
if k.db == nil {
if k.conn == nil {
return nil, fmt.Errorf("keybase: database not initialized")
}
return k.exportDump()
}
func (k *Keybase) exportDump() ([]byte, error) {
var dump strings.Builder
dump.WriteString(migrations.SchemaSQL + "\n")
tables := []string{
"did_documents", "verification_methods", "credentials",
"key_shares", "accounts", "ucan_tokens", "ucan_revocations",
"sessions", "services", "grants", "delegations", "sync_checkpoints",
}
for _, table := range tables {
if err := k.exportTable(&dump, table); err != nil {
continue
}
}
return []byte(dump.String()), nil
}
func (k *Keybase) exportTable(dump *strings.Builder, table string) error {
rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table))
if err != nil {
return err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return err
}
for rows.Next() {
values := make([]any, len(cols))
valuePtrs := make([]any, len(cols))
for i := range values {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
continue
}
dump.WriteString(fmt.Sprintf("INSERT INTO %s (", table))
dump.WriteString(strings.Join(cols, ", "))
dump.WriteString(") VALUES (")
for i, val := range values {
if i > 0 {
dump.WriteString(", ")
}
dump.WriteString(formatSQLValue(val))
}
dump.WriteString(");\n")
}
return rows.Err()
}
func formatSQLValue(val any) string {
if val == nil {
return "NULL"
}
switch v := val.(type) {
case int64:
return fmt.Sprintf("%d", v)
case float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case []byte:
return fmt.Sprintf("'%s'", escapeSQLString(string(v)))
case string:
return fmt.Sprintf("'%s'", escapeSQLString(v))
default:
return fmt.Sprintf("'%s'", escapeSQLString(fmt.Sprintf("%v", v)))
}
}
func escapeSQLString(s string) string {
return strings.ReplaceAll(s, "'", "''")
return serdes.Serialize(k.conn, "main")
}
func (k *Keybase) RestoreFromDump(data []byte) error {
k.mu.Lock()
defer k.mu.Unlock()
statements := strings.Split(string(data), ";\n")
for _, stmt := range statements {
stmt = strings.TrimSpace(stmt)
if stmt == "" || strings.HasPrefix(stmt, "--") {
continue
}
if strings.HasPrefix(stmt, "INSERT INTO") {
if _, err := k.db.Exec(stmt); err != nil {
return fmt.Errorf("keybase: failed to execute statement: %w", err)
}
}
if k.conn == nil {
return fmt.Errorf("keybase: database not initialized")
}
if err := serdes.Deserialize(k.conn, "main", data); err != nil {
return fmt.Errorf("keybase: deserialize database: %w", err)
}
docs, err := k.queries.ListAllDIDs(context.Background())

View File

@@ -2,11 +2,40 @@ package types
// GenerateInput represents the input for the generate function
type GenerateInput struct {
Credential string `json:"credential"` // Base64-encoded PublicKeyCredential
Credential string `json:"credential"` // Base64-encoded WebAuthn credential
// MPC keyshare data (optional - if provided, creates initial keyshare and account)
KeyShare *KeyShareInput `json:"key_share,omitempty"`
}
// KeyShareInput represents MPC keyshare data for initialization
type KeyShareInput struct {
KeyID string `json:"key_id"`
PartyIndex int64 `json:"party_index"`
Threshold int64 `json:"threshold"`
TotalParties int64 `json:"total_parties"`
Curve string `json:"curve"`
ShareData string `json:"share_data"`
PublicKey string `json:"public_key"`
ChainCode string `json:"chain_code,omitempty"`
DerivationPath string `json:"derivation_path,omitempty"`
}
// GenerateOutput represents the output of the generate function
type GenerateOutput struct {
DID string `json:"did"`
Database []byte `json:"database"`
// KeyShare info if a keyshare was provided
KeyShareID string `json:"key_share_id,omitempty"`
// Account info if an account was created
Account *AccountInfo `json:"account,omitempty"`
}
// AccountInfo represents created account information
type AccountInfo struct {
Address string `json:"address"`
ChainID string `json:"chain_id"`
CoinType int64 `json:"coin_type"`
}

107
main.go
View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -67,14 +68,14 @@ func generate() int32 {
return 1
}
did, err := initializeDatabase(credentialBytes)
result, err := initializeDatabase(credentialBytes, input.KeyShare)
if err != nil {
pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err))
return 1
}
state.SetInitialized(true)
state.SetDID(did)
state.SetDID(result.DID)
dbBytes, err := serializeDatabase()
if err != nil {
@@ -83,8 +84,10 @@ func generate() int32 {
}
output := types.GenerateOutput{
DID: did,
Database: dbBytes,
DID: result.DID,
Database: dbBytes,
KeyShareID: result.KeyShareID,
Account: result.Account,
}
if err := pdk.OutputJSON(output); err != nil {
@@ -92,7 +95,7 @@ func generate() int32 {
return 1
}
pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", did))
pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", result.DID))
return 0
}
@@ -238,20 +241,106 @@ func query() int32 {
return 0
}
func initializeDatabase(credentialBytes []byte) (string, error) {
type initResult struct {
DID string
KeyShareID string
Account *types.AccountInfo
}
func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInput) (*initResult, error) {
kb, err := keybase.Open()
if err != nil {
return "", fmt.Errorf("open database: %w", err)
return nil, fmt.Errorf("open database: %w", err)
}
ctx := context.Background()
did, err := kb.Initialize(ctx, credentialBytes)
if err != nil {
return "", fmt.Errorf("initialize: %w", err)
return nil, fmt.Errorf("initialize: %w", err)
}
result := &initResult{DID: did}
if keyShareInput != nil {
keyShareID, account, err := createInitialKeyShare(ctx, keyShareInput)
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeDatabase: failed to create keyshare: %s", err))
} else {
result.KeyShareID = keyShareID
result.Account = account
pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeDatabase: created keyshare %s", keyShareID))
}
}
pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records")
return did, nil
return result, nil
}
func createInitialKeyShare(ctx context.Context, input *types.KeyShareInput) (string, *types.AccountInfo, error) {
am, err := keybase.NewActionManager()
if err != nil {
return "", nil, fmt.Errorf("action manager: %w", err)
}
ks, err := am.CreateKeyShare(ctx, keybase.NewKeyShareInput{
KeyID: input.KeyID,
PartyIndex: input.PartyIndex,
Threshold: input.Threshold,
TotalParties: input.TotalParties,
Curve: input.Curve,
ShareData: input.ShareData,
PublicKey: input.PublicKey,
ChainCode: input.ChainCode,
DerivationPath: input.DerivationPath,
})
if err != nil {
return "", nil, fmt.Errorf("create keyshare: %w", err)
}
account, err := createInitialAccount(ctx, am, ks.ID, input.PublicKey)
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("createInitialKeyShare: failed to create account: %s", err))
return ks.ShareID, nil, nil
}
return ks.ShareID, account, nil
}
func createInitialAccount(ctx context.Context, am *keybase.ActionManager, keyShareID int64, publicKey string) (*types.AccountInfo, error) {
address := deriveCosmosAddress(publicKey)
if address == "" {
return nil, fmt.Errorf("failed to derive address from public key")
}
acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{
KeyShareID: keyShareID,
Address: address,
ChainID: "sonr-testnet-1",
CoinType: 118,
AccountIndex: 0,
AddressIndex: 0,
Label: "Default Account",
})
if err != nil {
return nil, fmt.Errorf("create account: %w", err)
}
return &types.AccountInfo{
Address: acc.Address,
ChainID: acc.ChainID,
CoinType: acc.CoinType,
}, nil
}
func deriveCosmosAddress(publicKeyHex string) string {
if publicKeyHex == "" {
return ""
}
pubBytes, err := hex.DecodeString(publicKeyHex)
if err != nil || len(pubBytes) < 20 {
return ""
}
return fmt.Sprintf("snr1%x", pubBytes[:20])
}
func serializeDatabase() ([]byte, error) {