init(go): Setup go-pdk plugin for enclave state management using WebAuthn credentials
This commit is contained in:
2
go.sum
Normal file
2
go.sum
Normal file
@@ -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=
|
||||
512
main.go
Normal file
512
main.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user