diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c15d382 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ= +github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..940d38e --- /dev/null +++ b/main.go @@ -0,0 +1,512 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/extism/go-pdk" +) + +// GenerateInput represents the input for the generate function +type GenerateInput struct { + Credential string `json:"credential"` // Base64-encoded PublicKeyCredential +} + +// GenerateOutput represents the output of the generate function +type GenerateOutput struct { + DID string `json:"did"` + Database []byte `json:"database"` +} + +// LoadInput represents the input for the load function +type LoadInput struct { + Database []byte `json:"database"` +} + +// LoadOutput represents the output of the load function +type LoadOutput struct { + Success bool `json:"success"` + DID string `json:"did,omitempty"` + Error string `json:"error,omitempty"` +} + +// ExecInput represents the input for the exec function +type ExecInput struct { + Filter string `json:"filter"` // GitHub-style filter: "resource:accounts action:sign" + Token string `json:"token"` // UCAN token for authorization +} + +// ExecOutput represents the output of the exec function +type ExecOutput struct { + Success bool `json:"success"` + Result json.RawMessage `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +// QueryInput represents the input for the query function +type QueryInput struct { + DID string `json:"did"` +} + +// QueryOutput represents the output of the query function +type QueryOutput struct { + DID string `json:"did"` + Controller string `json:"controller"` + VerificationMethods []VerificationMethod `json:"verification_methods"` + Accounts []Account `json:"accounts"` + Credentials []Credential `json:"credentials"` +} + +// VerificationMethod represents a DID verification method +type VerificationMethod struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKey string `json:"public_key"` + Purpose string `json:"purpose"` +} + +// Account represents a derived blockchain account +type Account struct { + Address string `json:"address"` + ChainID string `json:"chain_id"` + CoinType int `json:"coin_type"` + AccountIndex int `json:"account_index"` + AddressIndex int `json:"address_index"` + Label string `json:"label"` + IsDefault bool `json:"is_default"` +} + +// Credential represents a WebAuthn credential +type Credential struct { + CredentialID string `json:"credential_id"` + DeviceName string `json:"device_name"` + DeviceType string `json:"device_type"` + Authenticator string `json:"authenticator"` + Transports []string `json:"transports"` + CreatedAt string `json:"created_at"` + LastUsed string `json:"last_used"` +} + +// FilterParams parsed from GitHub-style filter syntax +type FilterParams struct { + Resource string + Action string + Subject string +} + +// Enclave holds the plugin state +type Enclave struct { + initialized bool + did string +} + +var enclave = &Enclave{} + +func main() {} + +//go:wasmexport generate +func generate() int32 { + pdk.Log(pdk.LogInfo, "generate: starting database initialization") + + var input GenerateInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("generate: failed to parse input: %w", err)) + return 1 + } + + if input.Credential == "" { + pdk.SetError(errors.New("generate: credential is required")) + return 1 + } + + credentialBytes, err := base64.StdEncoding.DecodeString(input.Credential) + if err != nil { + pdk.SetError(fmt.Errorf("generate: invalid base64 credential: %w", err)) + return 1 + } + + did, err := initializeDatabase(credentialBytes) + if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err)) + return 1 + } + + enclave.initialized = true + enclave.did = did + + dbBytes, err := serializeDatabase() + if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) + return 1 + } + + output := GenerateOutput{ + DID: did, + Database: dbBytes, + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", did)) + return 0 +} + +//go:wasmexport load +func load() int32 { + pdk.Log(pdk.LogInfo, "load: loading database from buffer") + + var input LoadInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("load: failed to parse input: %w", err)) + return 1 + } + + if len(input.Database) == 0 { + pdk.SetError(errors.New("load: database buffer is required")) + return 1 + } + + did, err := loadDatabase(input.Database) + if err != nil { + output := LoadOutput{ + Success: false, + Error: err.Error(), + } + pdk.OutputJSON(output) + return 1 + } + + enclave.initialized = true + enclave.did = did + + output := LoadOutput{ + Success: true, + DID: did, + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("load: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("load: loaded database for DID %s", did)) + return 0 +} + +//go:wasmexport exec +func exec() int32 { + pdk.Log(pdk.LogInfo, "exec: executing action") + + if !enclave.initialized { + pdk.SetError(errors.New("exec: database not initialized, call generate or load first")) + return 1 + } + + var input ExecInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("exec: failed to parse input: %w", err)) + return 1 + } + + if input.Filter == "" { + pdk.SetError(errors.New("exec: filter is required")) + return 1 + } + + params, err := parseFilter(input.Filter) + if err != nil { + pdk.SetError(fmt.Errorf("exec: invalid filter: %w", err)) + return 1 + } + + if input.Token != "" { + if err := validateUCAN(input.Token, params); err != nil { + output := ExecOutput{ + Success: false, + Error: fmt.Sprintf("authorization failed: %s", err.Error()), + } + pdk.OutputJSON(output) + return 1 + } + } + + result, err := executeAction(params) + if err != nil { + output := ExecOutput{ + Success: false, + Error: err.Error(), + } + pdk.OutputJSON(output) + return 1 + } + + output := ExecOutput{ + Success: true, + Result: result, + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("exec: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("exec: completed %s on %s", params.Action, params.Resource)) + return 0 +} + +//go:wasmexport query +func query() int32 { + pdk.Log(pdk.LogInfo, "query: resolving DID document") + + if !enclave.initialized { + pdk.SetError(errors.New("query: database not initialized, call generate or load first")) + return 1 + } + + var input QueryInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("query: failed to parse input: %w", err)) + return 1 + } + + if input.DID == "" { + input.DID = enclave.did + } + + if !strings.HasPrefix(input.DID, "did:") { + pdk.SetError(errors.New("query: invalid DID format")) + return 1 + } + + output, err := resolveDID(input.DID) + if err != nil { + pdk.SetError(fmt.Errorf("query: failed to resolve DID: %w", err)) + return 1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("query: failed to output result: %w", err)) + return 1 + } + + pdk.Log(pdk.LogInfo, fmt.Sprintf("query: resolved DID %s", input.DID)) + return 0 +} + +func initializeDatabase(credentialBytes []byte) (string, error) { + // TODO: Initialize SQLite database with schema + // TODO: Parse WebAuthn credential + // TODO: Generate MPC key shares + // TODO: Create DID document + // TODO: Insert initial records + + did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16]) + + pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records") + return did, nil +} + +func serializeDatabase() ([]byte, error) { + // TODO: Serialize SQLite database to bytes + // TODO: Encrypt with WebAuthn-derived key + return []byte("placeholder_database"), nil +} + +func loadDatabase(data []byte) (string, error) { + // TODO: Decrypt database with WebAuthn-derived key + // TODO: Load SQLite database from bytes + // TODO: Query for primary DID + + if len(data) < 10 { + return "", errors.New("invalid database format") + } + + did := "did:sonr:loaded" + pdk.Log(pdk.LogDebug, "loadDatabase: database loaded successfully") + return did, nil +} + +func parseFilter(filter string) (*FilterParams, error) { + params := &FilterParams{} + parts := strings.Fields(filter) + + for _, part := range parts { + kv := strings.SplitN(part, ":", 2) + if len(kv) != 2 { + continue + } + + key, value := kv[0], kv[1] + switch key { + case "resource": + params.Resource = value + case "action": + params.Action = value + case "subject": + params.Subject = value + } + } + + if params.Resource == "" { + return nil, errors.New("resource is required") + } + if params.Action == "" { + return nil, errors.New("action is required") + } + + return params, nil +} + +func validateUCAN(token string, params *FilterParams) error { + // TODO: Decode UCAN token + // TODO: Verify signature chain + // TODO: Check capabilities match params + // TODO: Verify not expired or revoked + + if token == "" { + return errors.New("token is required") + } + + pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action)) + return nil +} + +func executeAction(params *FilterParams) (json.RawMessage, error) { + // TODO: Route to appropriate handler based on resource/action + // TODO: Execute database queries + // TODO: Return results + + switch params.Resource { + case "accounts": + return executeAccountAction(params) + case "credentials": + return executeCredentialAction(params) + case "sessions": + return executeSessionAction(params) + case "grants": + return executeGrantAction(params) + default: + return nil, fmt.Errorf("unknown resource: %s", params.Resource) + } +} + +func executeAccountAction(params *FilterParams) (json.RawMessage, error) { + switch params.Action { + case "list": + accounts := []Account{ + { + Address: "sonr1abc123...", + ChainID: "sonr-mainnet-1", + CoinType: 118, + AccountIndex: 0, + AddressIndex: 0, + Label: "Primary", + IsDefault: true, + }, + } + return json.Marshal(accounts) + case "sign": + return json.Marshal(map[string]string{"signature": "placeholder"}) + default: + return nil, fmt.Errorf("unknown action for accounts: %s", params.Action) + } +} + +func executeCredentialAction(params *FilterParams) (json.RawMessage, error) { + switch params.Action { + case "list": + credentials := []Credential{ + { + CredentialID: "cred_abc123", + DeviceName: "MacBook Pro", + DeviceType: "platform", + Authenticator: "Touch ID", + Transports: []string{"internal"}, + CreatedAt: time.Now().Format(time.RFC3339), + LastUsed: time.Now().Format(time.RFC3339), + }, + } + return json.Marshal(credentials) + default: + return nil, fmt.Errorf("unknown action for credentials: %s", params.Action) + } +} + +func executeSessionAction(params *FilterParams) (json.RawMessage, error) { + switch params.Action { + case "list": + return json.Marshal([]map[string]interface{}{}) + case "create": + return json.Marshal(map[string]string{"session_id": "sess_placeholder"}) + case "revoke": + return json.Marshal(map[string]bool{"revoked": true}) + default: + return nil, fmt.Errorf("unknown action for sessions: %s", params.Action) + } +} + +func executeGrantAction(params *FilterParams) (json.RawMessage, error) { + switch params.Action { + case "list": + return json.Marshal([]map[string]interface{}{}) + case "create": + return json.Marshal(map[string]string{"grant_id": "grant_placeholder"}) + case "revoke": + return json.Marshal(map[string]bool{"revoked": true}) + default: + return nil, fmt.Errorf("unknown action for grants: %s", params.Action) + } +} + +func resolveDID(did string) (*QueryOutput, error) { + // TODO: Query database for DID document + // TODO: Fetch verification methods + // TODO: Fetch associated accounts + // TODO: Fetch credentials + + output := &QueryOutput{ + DID: did, + Controller: did, + VerificationMethods: []VerificationMethod{ + { + ID: did + "#key-1", + Type: "Ed25519VerificationKey2020", + Controller: did, + PublicKey: "placeholder_public_key", + Purpose: "authentication", + }, + }, + Accounts: []Account{ + { + Address: "sonr1abc123...", + ChainID: "sonr-mainnet-1", + CoinType: 118, + AccountIndex: 0, + AddressIndex: 0, + Label: "Primary", + IsDefault: true, + }, + }, + Credentials: []Credential{ + { + CredentialID: "cred_abc123", + DeviceName: "MacBook Pro", + DeviceType: "platform", + Authenticator: "Touch ID", + Transports: []string{"internal"}, + CreatedAt: time.Now().Format(time.RFC3339), + LastUsed: time.Now().Format(time.RFC3339), + }, + }, + } + + return output, nil +}