Feat/Add Crypto Libs #3
4
.github/Repo.toml
vendored
4
.github/Repo.toml
vendored
@@ -1,4 +0,0 @@
|
||||
[scopes]
|
||||
docs = ["MIGRATION.md", "README.md"]
|
||||
db = ["db"]
|
||||
config = [".github"]
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ src/dist
|
||||
src/node_modules
|
||||
dist
|
||||
node_modules
|
||||
.osgrep
|
||||
|
||||
16
AGENTS.md
16
AGENTS.md
@@ -170,16 +170,22 @@ if err != nil {
|
||||
|
||||
```
|
||||
motr-enclave/
|
||||
├── main.go # Plugin entry point, exported functions
|
||||
├── db/
|
||||
│ ├── schema.sql # Database schema
|
||||
│ ├── query.sql # SQLC query definitions
|
||||
│ └── *.go # Generated SQLC code
|
||||
├── cmd/
|
||||
│ └── enclave/
|
||||
│ └── main.go # Plugin entry point (WASM-only, go-pdk imports)
|
||||
├── internal/
|
||||
│ ├── keybase/ # Database access layer
|
||||
│ ├── crypto/ # Cryptographic operations
|
||||
│ ├── state/ # Plugin state management (WASM-only)
|
||||
│ ├── types/ # Input/output type definitions
|
||||
│ └── migrations/ # Database migrations
|
||||
├── sqlc.yaml # SQLC configuration
|
||||
├── Makefile # Build commands
|
||||
└── go.mod # Go module
|
||||
```
|
||||
|
||||
Note: Files with `//go:build wasip1` constraint (cmd/enclave/, internal/state/) only compile for WASM target.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Install with `make deps`:
|
||||
|
||||
@@ -32,8 +32,10 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
### What Goes in the Enclave (SQLite)
|
||||
|
||||
|
||||
| Data Type | Rationale |
|
||||
|-----------|-----------|
|
||||
| WebAuthn Credentials | Device authentication, never leaves device |
|
||||
@@ -46,6 +48,7 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted
|
||||
|
||||
### What Comes from APIs (NOT in SQLite)
|
||||
|
||||
|
||||
| Data Type | Source |
|
||||
|-----------|--------|
|
||||
| Token Balances | Chain RPC / Indexer API |
|
||||
@@ -61,7 +64,7 @@ This document outlines the SQL schema design for the Nebula wallet's **encrypted
|
||||
### SQLite WASM + Encryption
|
||||
|
||||
1. **Encryption**: Database encrypted with key derived from WebAuthn PRF extension
|
||||
2. **Pure Go Driver**: `modernc.org/sqlite` (WASM compatible, no CGO)
|
||||
2. **Pure Go Driver**: `github.com/ncruces/go-sqlite3` (WASM compatible, no CGO)
|
||||
3. **Minimal Schema**: Only security-critical data in enclave
|
||||
4. **INTEGER PRIMARY KEY**: Auto-increment without AUTOINCREMENT overhead
|
||||
5. **TEXT for binary**: Base64 encoded (BLOB performance poor in WASM)
|
||||
|
||||
2
Makefile
2
Makefile
@@ -15,7 +15,7 @@ deps:
|
||||
|
||||
build:
|
||||
@echo "Building WASM plugin..."
|
||||
@GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) .
|
||||
@GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) ./cmd/enclave
|
||||
@echo "Built $(BUILD_DIR)/$(BINARY)"
|
||||
|
||||
sdk:
|
||||
|
||||
677
TODO.md
677
TODO.md
@@ -6,190 +6,356 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
|
||||
| Category | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| Schema (10 tables) | Complete | `internal/migrations/schema.sql` |
|
||||
| SQLC Queries | Complete | `internal/migrations/query.sql` |
|
||||
| Schema (10 tables) | Complete | `internal/migrations/schema.sql` - Updated for v1.0.0-rc.1 |
|
||||
| SQLC Queries | Complete | `internal/migrations/query.sql` - CID-based queries added |
|
||||
| Generated Code | Complete | `internal/keybase/*.go` |
|
||||
| Basic Plugin Functions | Complete | `generate`, `load`, `exec`, `query`, `ping` |
|
||||
| Encryption | Not Started | WebAuthn PRF key derivation needed |
|
||||
| UCAN Authorization | Placeholder | Validation logic not implemented |
|
||||
| MPC Key Shares | Not Started | Key share management missing |
|
||||
| Database Serialization | Incomplete | Export dumps comments only |
|
||||
| **Encryption** | **Complete** | `internal/enclave/` - WebAuthn PRF key derivation + AES-256-GCM |
|
||||
| **UCAN v1.0.0-rc.1** | **Complete** | Core types, builders, policies, DB actions all complete |
|
||||
| UCAN DB Actions | Complete | `actions_delegation.go`, `actions_invocation.go` |
|
||||
| MPC Key Shares | Complete | `actions_keyshare.go` - Full key share management |
|
||||
| **Database Serialization** | **Complete** | Native SQLite serialization via `ncruces/go-sqlite3/ext/serdes` |
|
||||
|
||||
---
|
||||
|
||||
## 1. Encryption Strategy
|
||||
## 1. UCAN v1.0.0-rc.1 Migration (CRITICAL PRIORITY)
|
||||
|
||||
> **Status**: Core implementation complete using `github.com/ucan-wg/go-ucan v1.1.0`. Deprecated JWT-based files deleted. Remaining work is database integration and MPC signing.
|
||||
|
||||
### Completed Implementation
|
||||
|
||||
The following files implement UCAN v1.0.0-rc.1 using the official go-ucan library:
|
||||
|
||||
| File | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| `ucan.go` | ✅ Complete | Type re-exports, Sonr commands, pre-parsed constants |
|
||||
| `policy.go` | ✅ Complete | PolicyBuilder fluent API, Sonr-specific policy helpers |
|
||||
| `delegation.go` | ✅ Complete | DelegationBuilder fluent API, Sonr delegation helpers |
|
||||
| `invocation.go` | ✅ Complete | InvocationBuilder fluent API, Sonr invocation helpers |
|
||||
| `types.go` | ✅ Complete | ValidationError, Capability, ExecutionResult, Sonr types |
|
||||
|
||||
### Dependencies Added
|
||||
|
||||
- `github.com/ucan-wg/go-ucan v1.1.0` - Official UCAN library
|
||||
- `github.com/ipld/go-ipld-prime v0.21.0` - IPLD encoding
|
||||
- `github.com/MetaMask/go-did-it v1.0.0-pre1` - DID handling (indirect)
|
||||
- `github.com/ipfs/go-cid v0.5.0` - Content addressing (indirect)
|
||||
|
||||
### Deleted (Deprecated JWT-based)
|
||||
|
||||
- ~~`jwt.go`~~ - Removed
|
||||
- ~~`capability.go`~~ - Removed
|
||||
- ~~`verifier.go`~~ - Removed
|
||||
- ~~`source.go`~~ - Removed
|
||||
- ~~`internal/crypto/mpc/spec/`~~ - Entire directory removed
|
||||
|
||||
### 1.1 Core Data Structures
|
||||
|
||||
- [x] Create `internal/crypto/ucan/types.go` - v1.0.0-rc.1 types
|
||||
- [x] Re-export `Delegation` and `Invocation` from go-ucan
|
||||
- [x] `Task` struct (sub, cmd, args, nonce)
|
||||
- [x] `ReceiptPayload` struct (iss, ran, out, fx, meta, iat)
|
||||
- [x] `RevocationPayload` struct
|
||||
- [x] `ValidationError` with error codes matching TypeScript
|
||||
- [x] `Capability` struct (sub, cmd, pol)
|
||||
- [x] `ExecutionResult[T, E]` generic type
|
||||
- [x] Sonr-specific types: `VaultCapability`, `DIDCapability`, `DWNCapability`
|
||||
|
||||
- [x] Create `internal/crypto/ucan/policy.go` - Policy Language
|
||||
- [x] `PolicyBuilder` fluent API with all operators
|
||||
- [x] `Equal`, `NotEqual` - equality statements
|
||||
- [x] `GreaterThan`, `LessThan`, etc. - inequality statements
|
||||
- [x] `Like` - glob pattern matching
|
||||
- [x] `Not`, `And`, `Or` - logical connectives
|
||||
- [x] `All`, `Any` - quantifiers
|
||||
- [x] Sonr helpers: `VaultPolicy`, `DIDPolicy`, `ChainPolicy`, `AccountPolicy`
|
||||
|
||||
- [x] Create `internal/crypto/ucan/ucan.go` - Command types
|
||||
- [x] `Command` type re-exported from go-ucan
|
||||
- [x] Sonr commands: `/vault/*`, `/did/*`, `/dwn/*`, `/ucan/revoke`
|
||||
- [x] Pre-parsed command constants: `VaultRead`, `VaultWrite`, `DIDUpdate`, etc.
|
||||
- [x] `CommandSubsumes()` helper using go-ucan's `Covers()` method
|
||||
|
||||
### 1.2 Envelope Format & Encoding
|
||||
|
||||
- [x] Envelope handling via go-ucan library
|
||||
- [x] `ToSealed()` method produces DAG-CBOR bytes + CID
|
||||
- [x] `ToDagCbor()`, `ToDagJson()` encoding methods
|
||||
- [x] CID computation handled by go-ucan
|
||||
|
||||
- [x] Varsig support via go-ucan library
|
||||
- [x] Ed25519, P-256, secp256k1 via `go-did-it/crypto`
|
||||
|
||||
### 1.3 Delegation Operations
|
||||
|
||||
- [x] Create `internal/crypto/ucan/delegation.go` - Delegation creation/validation
|
||||
- [x] `DelegationBuilder` fluent API
|
||||
- [x] `NewDelegation`, `NewRootDelegation`, `NewPowerlineDelegation` re-exports
|
||||
- [x] `BuildSealed(privKey)` for signing
|
||||
- [x] Sonr helpers: `NewVaultDelegation`, `NewDIDDelegation`, `NewDWNDelegation`
|
||||
- [x] Temporal options: `ExpiresAt`, `ExpiresIn`, `NotBefore`, `NotBeforeIn`
|
||||
|
||||
### 1.4 Invocation Operations
|
||||
|
||||
- [x] Create `internal/crypto/ucan/invocation.go` - Invocation creation/validation
|
||||
- [x] `InvocationBuilder` fluent API
|
||||
- [x] `NewInvocation` re-export
|
||||
- [x] `BuildSealed(privKey)` for signing
|
||||
- [x] Proof chain management: `Proof()`, `Proofs()`
|
||||
- [x] Sonr helpers: `VaultReadInvocation`, `VaultSignInvocation`, `DIDUpdateInvocation`
|
||||
|
||||
### 1.5 Policy Evaluation Engine
|
||||
|
||||
> Note: go-ucan provides `ExecutionAllowed()` on invocations which validates proofs and evaluates policies.
|
||||
|
||||
- [x] Policy evaluation via go-ucan's `invocation.ExecutionAllowed(loader)`
|
||||
- [ ] Create `internal/crypto/ucan/eval.go` - Additional evaluation helpers (if needed)
|
||||
- [ ] Custom selector resolution for Sonr-specific args
|
||||
- [ ] Caching layer for repeated evaluations
|
||||
|
||||
### 1.6 Proof Chain Validation
|
||||
|
||||
> Note: go-ucan handles chain validation internally via `ExecutionAllowed()`.
|
||||
|
||||
- [x] Chain validation via go-ucan library
|
||||
- [x] Delegation storage in SQLite via `actions_delegation.go`
|
||||
- [x] `GetDelegationByCID`, `GetDelegationEnvelope` methods
|
||||
- [x] `ListDelegations*` methods for chain traversal
|
||||
- [ ] Create `internal/crypto/ucan/store.go` - Delegation loader for go-ucan
|
||||
- [ ] Implement `delegation.Loader` interface wrapping keybase actions
|
||||
- [ ] `GetDelegation(cid.Cid) (*delegation.Token, error)`
|
||||
- [ ] Cache loaded delegations for performance
|
||||
|
||||
### 1.7 Revocation
|
||||
|
||||
- [x] `RevocationInvocation()` helper in `invocation.go`
|
||||
- [x] Revocation storage via `actions_delegation.go`
|
||||
- [x] `RevokeDelegation(ctx, params)` - Create revocation record
|
||||
- [x] `IsDelegationRevoked(ctx, cid) (bool, error)` - Query revocation status
|
||||
- [ ] Create `internal/crypto/ucan/revocation.go` - Revocation checker for go-ucan
|
||||
- [ ] Implement revocation checking interface
|
||||
- [ ] Integration with chain validation via `ExecutionAllowed()`
|
||||
|
||||
### 1.8 Database Integration
|
||||
|
||||
- [x] Update `internal/migrations/schema.sql` for v1.0.0-rc.1
|
||||
- [x] `ucan_delegations` table (cid, envelope BLOB, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline)
|
||||
- [x] `ucan_invocations` table (cid, envelope BLOB, iss, sub, aud, cmd, prf, exp, iat, executed_at, result_cid)
|
||||
- [x] `ucan_revocations` table (delegation_cid, revoked_by, invocation_cid, reason)
|
||||
- [x] Indexes on iss, aud, sub, cmd for efficient queries
|
||||
|
||||
- [x] Update `internal/migrations/query.sql` for v1.0.0-rc.1
|
||||
- [x] `CreateDelegation`, `GetDelegationByCID`, `GetDelegationEnvelopeByCID`
|
||||
- [x] `ListDelegationsByDID`, `ListDelegationsByIssuer`, `ListDelegationsByAudience`, `ListDelegationsBySubject`
|
||||
- [x] `ListDelegationsForCommand`, `ListRootDelegations`, `ListPowerlineDelegations`
|
||||
- [x] `CreateInvocation`, `GetInvocationByCID`, `GetInvocationEnvelopeByCID`
|
||||
- [x] `ListInvocationsByDID`, `ListInvocationsByIssuer`, `ListInvocationsForCommand`
|
||||
- [x] `MarkInvocationExecuted`, `ListPendingInvocations`
|
||||
- [x] `CreateRevocation`, `IsDelegationRevoked`, `GetRevocation`, `ListRevocationsByRevoker`
|
||||
|
||||
- [x] Create `internal/keybase/actions_delegation.go` - Delegation action handlers
|
||||
- [x] `StoreDelegation`, `GetDelegationByCID`, `GetDelegationEnvelope`
|
||||
- [x] `ListDelegations`, `ListDelegationsByIssuer`, `ListDelegationsByAudience`
|
||||
- [x] `ListDelegationsForCommand`, `IsDelegationRevoked`, `RevokeDelegation`
|
||||
- [x] `DeleteDelegation`, `CleanExpiredDelegations`
|
||||
|
||||
- [x] Create `internal/keybase/actions_invocation.go` - Invocation action handlers
|
||||
- [x] `StoreInvocation`, `GetInvocationByCID`, `GetInvocationEnvelope`
|
||||
- [x] `ListInvocations`, `ListInvocationsByCommand`, `ListPendingInvocations`
|
||||
- [x] `MarkInvocationExecuted`, `CleanOldInvocations`
|
||||
|
||||
### 1.9 MPC Signing Integration
|
||||
|
||||
- [ ] Create `internal/crypto/ucan/signer.go` - MPC key integration
|
||||
- [ ] Implement `crypto.PrivateKeySigningBytes` interface for MPC
|
||||
- [ ] Sign delegations with MPC key shares
|
||||
- [ ] Sign invocations with MPC key shares
|
||||
|
||||
### 1.10 Testing
|
||||
|
||||
- [ ] Unit tests for builders (DelegationBuilder, InvocationBuilder)
|
||||
- [ ] Unit tests for policy helpers
|
||||
- [ ] Unit tests for Sonr-specific invocations
|
||||
- [ ] Interoperability tests against TypeScript implementation
|
||||
- [ ] Test vectors from UCAN spec
|
||||
|
||||
---
|
||||
|
||||
## 2. Encryption Strategy
|
||||
|
||||
> Reference: MIGRATION.md lines 770-814
|
||||
> **Status**: ✅ Complete - Implemented in `internal/enclave/`
|
||||
|
||||
### 1.1 WebAuthn PRF Key Derivation
|
||||
- [ ] Implement `DeriveEncryptionKey(prfOutput []byte) ([]byte, error)`
|
||||
- [ ] Use HKDF with SHA-256 to derive 256-bit encryption key
|
||||
- [ ] Salt with `"nebula-enclave-v1"` as info parameter
|
||||
### 2.1 WebAuthn PRF Key Derivation
|
||||
|
||||
### 1.2 Database Encryption
|
||||
- [ ] Implement application-level AES-GCM encryption for serialized pages
|
||||
- [ ] Add encryption wrapper around `Serialize()` output
|
||||
- [ ] Add decryption wrapper for `Load()` input
|
||||
- [ ] Store encryption metadata (IV, auth tag) with serialized data
|
||||
- [x] Implement `DeriveEncryptionKey(prfOutput []byte) ([]byte, error)`
|
||||
- [x] Use HKDF with SHA-256 to derive 256-bit encryption key
|
||||
- [x] Salt with `"nebula-enclave-v1"` as info parameter
|
||||
- [x] `DeriveKeyWithContext()` for purpose-specific key derivation
|
||||
|
||||
### 1.3 Encrypted Database Wrapper
|
||||
- [ ] Create `internal/enclave/enclave.go` - Encrypted database wrapper
|
||||
- [ ] Create `internal/enclave/crypto.go` - WebAuthn PRF key derivation
|
||||
- [ ] Integrate with existing `internal/keybase` package
|
||||
### 2.2 Database Encryption
|
||||
|
||||
- [x] Implement application-level AES-256-GCM encryption for serialized pages
|
||||
- [x] Add encryption wrapper around `Serialize()` output (`EncryptBytes()`)
|
||||
- [x] Add decryption wrapper for `Load()` input (`DecryptBytes()`)
|
||||
- [x] Store encryption metadata (version, nonce, auth tag) with serialized data
|
||||
- [x] `SecureZero()` for memory clearing of sensitive data
|
||||
|
||||
### 2.3 Encrypted Database Wrapper
|
||||
|
||||
- [x] Create `internal/enclave/enclave.go` - Encrypted database wrapper
|
||||
- [x] `Enclave` struct wrapping `Keybase` with encryption key
|
||||
- [x] `SerializeEncrypted()` - Export encrypted database
|
||||
- [x] `LoadEncrypted()` - Load from encrypted bytes
|
||||
- [x] `Export()` / `Import()` - Full bundle operations with DID
|
||||
- [x] `EncryptedBundle` struct with JSON marshaling
|
||||
- [x] Create `internal/enclave/crypto.go` - WebAuthn PRF key derivation
|
||||
- [x] `Encrypt()` / `Decrypt()` with `EncryptedData` struct
|
||||
- [x] `EncryptBytes()` / `DecryptBytes()` convenience functions
|
||||
- [x] `GenerateNonce()` for secure random nonce generation
|
||||
- [x] Integrate with existing `internal/keybase` package via `FromExisting()`
|
||||
|
||||
---
|
||||
|
||||
## 2. Database Serialization
|
||||
## 3. Database Serialization
|
||||
|
||||
> Current implementation in `conn.go:exportDump()` only outputs comments
|
||||
> **Status**: ✅ Complete - Using native SQLite serialization via `ncruces/go-sqlite3/ext/serdes`
|
||||
|
||||
### 2.1 Proper Serialization
|
||||
- [ ] Implement full row export with proper SQL INSERT statements
|
||||
- [ ] Handle JSON columns correctly (escape special characters)
|
||||
- [ ] Include table creation order for foreign key constraints
|
||||
- [ ] Add version header for migration compatibility
|
||||
### 3.1 Native SQLite Serialization
|
||||
|
||||
### 2.2 Proper Deserialization
|
||||
- [ ] Parse serialized SQL dump in `Load()`
|
||||
- [ ] Execute INSERT statements to restore data
|
||||
- [ ] Validate data integrity after restore
|
||||
- [ ] Handle schema version mismatches
|
||||
- [x] `Serialize()` using `serdes.Serialize(conn, "main")` - Binary database export
|
||||
- [x] Full database state captured as byte slice
|
||||
- [x] No SQL parsing needed - direct database format
|
||||
- [x] Preserves all data types, indexes, and constraints
|
||||
|
||||
### 3.2 Native SQLite Deserialization
|
||||
|
||||
- [x] `Load()` using `serdes.Deserialize(conn, "main", data)` - Binary import
|
||||
- [x] `RestoreFromDump()` for encrypted bundle loading
|
||||
- [x] Automatic DID context restoration after load
|
||||
- [x] Integrated with `internal/enclave` for encrypted storage
|
||||
|
||||
---
|
||||
|
||||
## 3. Action Manager Extensions
|
||||
## 4. Action Manager Extensions
|
||||
|
||||
> Reference: `internal/keybase/actions.go`
|
||||
|
||||
### 3.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`
|
||||
### 4.1 Key Share Actions
|
||||
|
||||
### 3.2 UCAN Token Actions
|
||||
- [ ] `CreateUCAN(ctx, params) (*UCANResult, error)`
|
||||
- [ ] `ListUCANs(ctx) ([]UCANResult, error)`
|
||||
- [ ] `GetUCANByCID(ctx, cid) (*UCANResult, error)`
|
||||
- [ ] `ListUCANsByAudience(ctx, audience) ([]UCANResult, error)`
|
||||
- [ ] `RevokeUCAN(ctx, cid) error`
|
||||
- [ ] `IsUCANRevoked(ctx, cid) (bool, error)`
|
||||
- [ ] `CreateRevocation(ctx, params) error`
|
||||
- [ ] `CleanExpiredUCANs(ctx) 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`
|
||||
|
||||
### 3.3 Delegation Actions
|
||||
- [ ] `CreateDelegation(ctx, params) (*DelegationResult, error)`
|
||||
- [ ] `ListDelegationsByDelegator(ctx, delegator) ([]DelegationResult, error)`
|
||||
- [ ] `ListDelegationsByDelegate(ctx, delegate) ([]DelegationResult, error)`
|
||||
- [ ] `ListDelegationsForResource(ctx, resource) ([]DelegationResult, error)`
|
||||
- [ ] `GetDelegationChain(ctx, delegationID) ([]DelegationResult, error)`
|
||||
- [ ] `RevokeDelegation(ctx, delegationID) error`
|
||||
- [ ] `RevokeDelegationChain(ctx, delegationID) error`
|
||||
### 4.2 UCAN Token Actions (v1.0.0-rc.1)
|
||||
|
||||
### 3.4 Verification Method Actions
|
||||
- [ ] `CreateVerificationMethod(ctx, params) (*VerificationMethodResult, error)`
|
||||
- [ ] `ListVerificationMethods(ctx) ([]VerificationMethodResult, error)`
|
||||
- [ ] `GetVerificationMethod(ctx, methodID) (*VerificationMethodResult, error)`
|
||||
- [ ] `DeleteVerificationMethod(ctx, methodID) error`
|
||||
- [x] `StoreDelegation(ctx, params) (*DelegationResult, error)`
|
||||
- [x] `ListDelegations(ctx) ([]DelegationResult, error)`
|
||||
- [x] `GetDelegationByCID(ctx, cid) (*DelegationResult, error)`
|
||||
- [x] `GetDelegationEnvelope(ctx, cid) ([]byte, error)`
|
||||
- [x] `ListDelegationsByIssuer(ctx, issuer) ([]DelegationResult, error)`
|
||||
- [x] `ListDelegationsByAudience(ctx, audience) ([]DelegationResult, error)`
|
||||
- [x] `ListDelegationsForCommand(ctx, cmd) ([]DelegationResult, error)`
|
||||
- [x] `StoreInvocation(ctx, params) (*InvocationResult, error)`
|
||||
- [x] `GetInvocationByCID(ctx, cid) (*InvocationResult, error)`
|
||||
- [x] `GetInvocationEnvelope(ctx, cid) ([]byte, error)`
|
||||
- [x] `ListInvocations(ctx, limit) ([]InvocationResult, error)`
|
||||
- [x] `ListInvocationsByCommand(ctx, cmd, limit) ([]InvocationResult, error)`
|
||||
- [x] `ListPendingInvocations(ctx) ([]InvocationResult, error)`
|
||||
- [x] `MarkInvocationExecuted(ctx, cid, resultCID) error`
|
||||
- [x] `RevokeDelegation(ctx, params) error`
|
||||
- [x] `IsDelegationRevoked(ctx, cid) (bool, error)`
|
||||
- [x] `DeleteDelegation(ctx, cid) error`
|
||||
- [x] `CleanExpiredDelegations(ctx) error`
|
||||
- [x] `CleanOldInvocations(ctx) error`
|
||||
- [ ] `ValidateInvocation(ctx, invocation) (*ValidationResult, error)` - Requires delegation.Loader
|
||||
|
||||
### 3.5 Service Actions
|
||||
- [ ] `CreateService(ctx, params) (*ServiceResult, error)`
|
||||
- [ ] `GetServiceByOrigin(ctx, origin) (*ServiceResult, error)`
|
||||
- [ ] `GetServiceByID(ctx, serviceID) (*ServiceResult, error)`
|
||||
- [ ] `UpdateService(ctx, params) error`
|
||||
- [ ] `ListVerifiedServices(ctx) ([]ServiceResult, error)`
|
||||
### 4.3 Verification Method Actions
|
||||
|
||||
### 3.6 Grant Actions (Extend Existing)
|
||||
- [ ] `CreateGrant(ctx, params) (*GrantResult, error)`
|
||||
- [ ] `GetGrantByService(ctx, serviceID) (*GrantResult, error)`
|
||||
- [ ] `UpdateGrantScopes(ctx, grantID, scopes, accounts) error`
|
||||
- [ ] `UpdateGrantLastUsed(ctx, grantID) error`
|
||||
- [ ] `SuspendGrant(ctx, grantID) error`
|
||||
- [ ] `ReactivateGrant(ctx, grantID) error`
|
||||
- [ ] `CountActiveGrants(ctx) (int64, error)`
|
||||
- [x] `CreateVerificationMethod(ctx, params) (*VerificationMethodResult, error)`
|
||||
- [x] `ListVerificationMethodsFull(ctx) ([]VerificationMethodResult, error)`
|
||||
- [x] `GetVerificationMethod(ctx, methodID) (*VerificationMethodResult, error)`
|
||||
- [x] `DeleteVerificationMethod(ctx, methodID) error`
|
||||
|
||||
### 3.7 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`
|
||||
### 4.4 Service Actions
|
||||
|
||||
### 3.8 Credential Actions (Extend Existing)
|
||||
- [ ] `CreateCredential(ctx, params) (*CredentialResult, error)`
|
||||
- [ ] `UpdateCredentialCounter(ctx, credentialID, signCount) error`
|
||||
- [ ] `RenameCredential(ctx, credentialID, name) error`
|
||||
- [ ] `DeleteCredential(ctx, credentialID) error`
|
||||
- [ ] `CountCredentialsByDID(ctx) (int64, error)`
|
||||
- [x] `CreateService(ctx, params) (*ServiceResult, error)`
|
||||
- [x] `GetServiceByOrigin(ctx, origin) (*ServiceResult, error)`
|
||||
- [x] `GetServiceByID(ctx, serviceID) (*ServiceResult, error)`
|
||||
- [x] `UpdateService(ctx, params) error`
|
||||
- [x] `ListVerifiedServices(ctx) ([]ServiceResult, error)`
|
||||
|
||||
### 3.9 Session Actions (Extend Existing)
|
||||
- [ ] `GetSessionByID(ctx, sessionID) (*SessionResult, error)`
|
||||
- [ ] `GetCurrentSession(ctx) (*SessionResult, error)`
|
||||
- [ ] `UpdateSessionActivity(ctx, sessionID) error`
|
||||
- [ ] `SetCurrentSession(ctx, sessionID) error`
|
||||
- [ ] `DeleteExpiredSessions(ctx) error`
|
||||
### 4.5 Grant Actions (Extend Existing)
|
||||
|
||||
- [x] `CreateGrant(ctx, params) (*GrantResult, error)`
|
||||
- [x] `GetGrantByService(ctx, serviceID) (*GrantResult, error)`
|
||||
- [x] `UpdateGrantScopes(ctx, grantID, scopes, accounts) error`
|
||||
- [x] `UpdateGrantLastUsed(ctx, grantID) error`
|
||||
- [x] `SuspendGrant(ctx, grantID) error`
|
||||
- [x] `ReactivateGrant(ctx, grantID) error`
|
||||
- [x] `CountActiveGrants(ctx) (int64, error)`
|
||||
|
||||
### 4.6 Account Actions (Extend Existing)
|
||||
|
||||
- [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)
|
||||
|
||||
- [x] `CreateCredential(ctx, params) (*CredentialResult, error)`
|
||||
- [x] `UpdateCredentialCounter(ctx, credentialID, signCount) error`
|
||||
- [x] `RenameCredential(ctx, credentialID, name) error`
|
||||
- [x] `DeleteCredential(ctx, credentialID) error`
|
||||
- [x] `CountCredentialsByDID(ctx) (int64, error)`
|
||||
|
||||
### 4.8 Session Actions (Extend Existing)
|
||||
|
||||
- [x] `GetSessionByID(ctx, sessionID) (*SessionResult, error)`
|
||||
- [x] `GetCurrentSession(ctx) (*SessionResult, error)`
|
||||
- [x] `UpdateSessionActivity(ctx, sessionID) error`
|
||||
- [x] `SetCurrentSession(ctx, sessionID) error`
|
||||
- [x] `DeleteExpiredSessions(ctx) error`
|
||||
|
||||
### 4.9 Sync Checkpoint Actions
|
||||
|
||||
### 3.10 Sync Checkpoint Actions
|
||||
- [ ] `GetSyncCheckpoint(ctx, resourceType) (*SyncCheckpointResult, error)`
|
||||
- [ ] `UpsertSyncCheckpoint(ctx, params) error`
|
||||
- [ ] `ListSyncCheckpoints(ctx) ([]SyncCheckpointResult, error)`
|
||||
|
||||
---
|
||||
|
||||
## 4. UCAN Authorization
|
||||
|
||||
> Reference: MIGRATION.md lines 820-821
|
||||
|
||||
### 4.1 Token Validation
|
||||
- [ ] Implement proper UCAN token parsing (JWT-like structure)
|
||||
- [ ] Validate token signature against issuer's public key
|
||||
- [ ] Check token expiration (`exp` claim)
|
||||
- [ ] Check token not-before (`nbf` claim)
|
||||
- [ ] Validate audience matches expected DID
|
||||
|
||||
### 4.2 Capability Verification
|
||||
- [ ] Parse capabilities array from token
|
||||
- [ ] Match requested action against granted capabilities
|
||||
- [ ] Implement resource pattern matching (e.g., `sonr://vault/*`)
|
||||
- [ ] Respect action restrictions (e.g., `sign`, `read`, `write`)
|
||||
|
||||
### 4.3 Proof Chain Validation
|
||||
- [ ] Follow proof chain to root UCAN
|
||||
- [ ] Validate each link in the chain
|
||||
- [ ] Ensure capability attenuation (child can't exceed parent)
|
||||
- [ ] Check revocation status for all tokens in chain
|
||||
|
||||
### 4.4 Revocation Checking
|
||||
- [ ] Query `ucan_revocations` table
|
||||
- [ ] Check all tokens in proof chain
|
||||
- [ ] Cache revocation status for performance
|
||||
|
||||
---
|
||||
|
||||
## 5. MPC Key Share Management
|
||||
|
||||
> Reference: MIGRATION.md lines 823-824
|
||||
|
||||
### 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
|
||||
|
||||
---
|
||||
@@ -199,21 +365,27 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
> Reference: `main.go`
|
||||
|
||||
### 6.1 Extend `exec` Resource Handlers
|
||||
- [ ] Add `key_shares` resource handler
|
||||
- [ ] Add `ucans` resource handler
|
||||
- [ ] Add `delegations` resource handler
|
||||
- [ ] Add `verification_methods` resource handler
|
||||
- [ ] Add `services` resource handler
|
||||
|
||||
- [x] Add `key_shares` resource handler (list, get, rotate, archive, delete)
|
||||
- [x] Add `ucans` resource handler (v1.0.0-rc.1 delegations - list, get, revoke, verify, cleanup)
|
||||
- [x] Add `delegations` resource handler (v1.0.0-rc.1 - list, list_received, list_command, get, revoke, verify)
|
||||
- [ ] Add `invocations` resource handler (v1.0.0-rc.1)
|
||||
- [x] Add `verification_methods` resource handler (list, get, delete)
|
||||
- [x] Add `services` resource handler (list, get, get_by_id)
|
||||
- [ ] Add `sync_checkpoints` resource handler
|
||||
|
||||
### 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
|
||||
|
||||
- [ ] Implement `sign` wasmexport function
|
||||
- [ ] Support signing with MPC key shares
|
||||
- [ ] Return signature in appropriate format
|
||||
@@ -221,17 +393,25 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
|
||||
---
|
||||
|
||||
## 7. Capability Delegation
|
||||
## 7. Capability Delegation (v1.0.0-rc.1)
|
||||
|
||||
> Reference: MIGRATION.md lines 826-827
|
||||
> Reference: UCAN Delegation specification
|
||||
|
||||
### 7.1 Delegation Chain Management
|
||||
- [ ] Enforce maximum delegation depth (prevent infinite chains)
|
||||
- [ ] Validate delegator has capability to delegate
|
||||
- [ ] Ensure proper capability attenuation
|
||||
- [ ] Track parent-child relationships
|
||||
|
||||
### 7.2 Delegation Status
|
||||
- [ ] Enforce maximum delegation depth (prevent infinite chains)
|
||||
- [ ] Validate delegator has capability to delegate (sub field)
|
||||
- [ ] Ensure proper capability attenuation (cmd + pol)
|
||||
- [ ] Track parent-child relationships via CID references
|
||||
|
||||
### 7.2 Policy Attenuation
|
||||
|
||||
- [ ] Child policy must be more restrictive than parent
|
||||
- [ ] Implement policy subsumption checking
|
||||
- [ ] Command hierarchy validation (`/crud/*` subsumes `/crud/read`)
|
||||
|
||||
### 7.3 Delegation Status
|
||||
|
||||
- [ ] Implement expiration checking
|
||||
- [ ] Handle revocation cascades (revoke chain)
|
||||
- [ ] Update status on expiry
|
||||
@@ -243,12 +423,14 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
> Reference: MIGRATION.md line 827
|
||||
|
||||
### 8.1 Sync Infrastructure
|
||||
|
||||
- [ ] Create `internal/enclave/sync.go` - DID state sync logic
|
||||
- [ ] Implement checkpoint tracking
|
||||
- [ ] Store last synced block height
|
||||
- [ ] Track last processed transaction hash
|
||||
|
||||
### 8.2 Sync Operations
|
||||
|
||||
- [ ] Fetch DID document updates from chain
|
||||
- [ ] Validate on-chain document hash
|
||||
- [ ] Update local state on changes
|
||||
@@ -261,18 +443,23 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
> Reference: README.md, `src/` directory
|
||||
|
||||
### 9.1 Core SDK
|
||||
|
||||
- [ ] Implement `createEnclave(wasmPath)` factory
|
||||
- [ ] Implement `generate(credential)` wrapper
|
||||
- [ ] Implement `load(database)` wrapper
|
||||
- [ ] Implement `exec(filter, token?)` wrapper
|
||||
- [ ] Implement `query(did?)` wrapper
|
||||
|
||||
### 9.2 Type Definitions
|
||||
- [ ] Generate TypeScript types from Go structs
|
||||
- [ ] Export type definitions for consumers
|
||||
- [ ] Add JSDoc documentation
|
||||
### 9.2 UCAN SDK (v1.0.0-rc.1)
|
||||
|
||||
- [ ] Delegation builder using `src/ucan.ts` types
|
||||
- [ ] Invocation builder
|
||||
- [ ] Policy builder helpers
|
||||
- [ ] Envelope encoding/decoding (DAG-CBOR)
|
||||
- [ ] CID computation
|
||||
|
||||
### 9.3 WebAuthn Integration
|
||||
|
||||
- [ ] Helper for credential creation
|
||||
- [ ] Helper for PRF extension output
|
||||
- [ ] Proper encoding/decoding utilities
|
||||
@@ -282,39 +469,53 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
## 10. Testing
|
||||
|
||||
### 10.1 Unit Tests
|
||||
|
||||
- [ ] Test all ActionManager methods
|
||||
- [ ] Test serialization/deserialization roundtrip
|
||||
- [ ] Test encryption/decryption
|
||||
- [ ] Test UCAN validation logic
|
||||
- [ ] Test UCAN policy evaluation
|
||||
- [ ] Test UCAN envelope encoding
|
||||
|
||||
### 10.2 Integration Tests
|
||||
- [ ] Test full generate → load → exec flow
|
||||
|
||||
- [ ] Test full generate -> load -> exec flow
|
||||
- [ ] Test credential lifecycle
|
||||
- [ ] Test session management
|
||||
- [ ] Test grant management
|
||||
- [ ] Test UCAN delegation chain
|
||||
|
||||
### 10.3 Plugin Tests
|
||||
|
||||
- [ ] Extend `make test-plugin` with all functions
|
||||
- [ ] Add error case testing
|
||||
- [ ] Test with various input formats
|
||||
|
||||
### 10.4 Interoperability Tests
|
||||
|
||||
- [ ] Go <-> TypeScript UCAN envelope compatibility
|
||||
- [ ] CID computation consistency
|
||||
- [ ] Policy evaluation consistency
|
||||
|
||||
---
|
||||
|
||||
## 11. Security Hardening
|
||||
|
||||
### 11.1 Input Validation
|
||||
|
||||
- [ ] Validate all JSON inputs against schemas
|
||||
- [ ] Sanitize SQL-sensitive characters in serialization
|
||||
- [ ] Validate DID format on all inputs
|
||||
- [ ] Validate base64 encoding
|
||||
|
||||
### 11.2 Cryptographic Security
|
||||
|
||||
- [ ] Use constant-time comparison for sensitive data
|
||||
- [ ] Clear sensitive data from memory after use
|
||||
- [ ] Validate key sizes and formats
|
||||
- [ ] Implement proper nonce generation
|
||||
|
||||
### 11.3 Access Control
|
||||
|
||||
- [ ] Enforce DID ownership on all mutations
|
||||
- [ ] Validate session before sensitive operations
|
||||
- [ ] Check grant scopes before data access
|
||||
@@ -324,20 +525,164 @@ Remaining tasks from [MIGRATION.md](./MIGRATION.md) for the Nebula Key Enclave.
|
||||
|
||||
## Priority Order
|
||||
|
||||
1. **High Priority** (Core Functionality)
|
||||
- Database Serialization (2.1, 2.2)
|
||||
- Credential Creation (6.2, 3.8)
|
||||
- Key Share Actions (3.1)
|
||||
- Account Actions (3.7)
|
||||
1. **CRITICAL (Spec Compliance)** - ✅ Complete
|
||||
- ~~UCAN v1.0.0-rc.1 Migration (Section 1)~~ ✅ All core items complete
|
||||
- ~~Core data structures (1.1)~~ ✅ Using go-ucan v1.1.0
|
||||
- ~~Envelope format (1.2)~~ ✅ Handled by go-ucan
|
||||
- ~~Delegation operations (1.3)~~ ✅ DelegationBuilder complete
|
||||
- ~~Invocation operations (1.4)~~ ✅ InvocationBuilder complete
|
||||
- ~~Database integration (1.8)~~ ✅ Schema, queries, and actions complete
|
||||
- MPC signing integration (1.9) - Next priority
|
||||
|
||||
2. **Medium Priority** (Authorization)
|
||||
- UCAN Validation (4.1, 4.2)
|
||||
- Delegation Management (7.1, 7.2)
|
||||
- Encryption Strategy (1.1, 1.2)
|
||||
2. **High Priority (Core Functionality)** - ✅ Mostly Complete
|
||||
- ~~Database Serialization (3.1, 3.2)~~ ✅ Native SQLite serdes
|
||||
- ~~Credential Actions (4.7)~~ ✅ All CRUD operations
|
||||
- ~~Key Share Actions (4.1)~~ ✅ All operations
|
||||
- ~~Account Actions (4.6)~~ ✅ All operations
|
||||
- Delegation Loader for go-ucan (1.6) - Remaining
|
||||
- Invocations exec handler (6.1) - Remaining
|
||||
|
||||
3. **Lower Priority** (Enhancement)
|
||||
3. **Medium Priority (Authorization)** - ✅ Partially Complete
|
||||
- Revocation checker for go-ucan (1.7) - Remaining
|
||||
- MPC Signing (1.9) - Remaining
|
||||
- ~~Encryption Strategy (2.1, 2.2, 2.3)~~ ✅ Complete
|
||||
|
||||
4. **Lower Priority (Enhancement)**
|
||||
- TypeScript SDK (9.x)
|
||||
- DID State Sync (8.x)
|
||||
- Additional exec handlers (6.1)
|
||||
- Sync checkpoints handler (6.1)
|
||||
- Testing (10.x)
|
||||
- Security Hardening (11.x)
|
||||
|
||||
---
|
||||
|
||||
## Completed Items
|
||||
|
||||
### Encryption & Serialization (January 2025)
|
||||
|
||||
Full encryption layer and native SQLite serialization implemented:
|
||||
|
||||
- ✅ `internal/enclave/crypto.go` - WebAuthn PRF key derivation
|
||||
- `DeriveEncryptionKey()` using HKDF with SHA-256
|
||||
- `DeriveKeyWithContext()` for purpose-specific keys
|
||||
- AES-256-GCM encryption/decryption (`Encrypt`, `Decrypt`)
|
||||
- `EncryptBytes()` / `DecryptBytes()` convenience functions
|
||||
- `SecureZero()` for memory clearing
|
||||
|
||||
- ✅ `internal/enclave/enclave.go` - Encrypted database wrapper
|
||||
- `Enclave` struct wrapping `Keybase` with encryption
|
||||
- `SerializeEncrypted()` / `LoadEncrypted()` methods
|
||||
- `Export()` / `Import()` with `EncryptedBundle`
|
||||
- `FromExisting()` to wrap existing keybase
|
||||
|
||||
- ✅ `internal/keybase/conn.go` - Native SQLite serialization
|
||||
- `Serialize()` using `serdes.Serialize()` from ncruces/go-sqlite3
|
||||
- `Load()` using `serdes.Deserialize()`
|
||||
- `RestoreFromDump()` for encrypted bundle loading
|
||||
|
||||
### Action Manager Extensions (January 2025)
|
||||
|
||||
All CRUD action handlers completed for remaining entities:
|
||||
|
||||
- ✅ `internal/keybase/actions_verification.go`
|
||||
- CreateVerificationMethod, ListVerificationMethodsFull
|
||||
- GetVerificationMethod, DeleteVerificationMethod
|
||||
|
||||
- ✅ `internal/keybase/actions_service.go`
|
||||
- CreateService, GetServiceByOrigin, GetServiceByID
|
||||
- UpdateService, ListVerifiedServices
|
||||
|
||||
- ✅ `internal/keybase/actions_grant.go`
|
||||
- CreateGrant, GetGrantByService, UpdateGrantScopes
|
||||
- UpdateGrantLastUsed, SuspendGrant, ReactivateGrant, CountActiveGrants
|
||||
|
||||
- ✅ `internal/keybase/actions_credential.go`
|
||||
- CreateCredential, UpdateCredentialCounter, RenameCredential
|
||||
- DeleteCredential, CountCredentialsByDID
|
||||
|
||||
- ✅ `internal/keybase/actions_session.go`
|
||||
- GetSessionByID, GetCurrentSession, UpdateSessionActivity
|
||||
- SetCurrentSession, DeleteExpiredSessions
|
||||
|
||||
### Plugin Exec Handlers (January 2025)
|
||||
|
||||
Extended exec function with new resource handlers:
|
||||
|
||||
- ✅ `key_shares` - list, get, rotate, archive, delete
|
||||
- ✅ `verification_methods` - list, get, delete
|
||||
- ✅ `services` - list, get, get_by_id
|
||||
|
||||
### UCAN v1.0.0-rc.1 Database Integration (January 2025)
|
||||
|
||||
Schema and action handlers for storing/querying UCAN delegations and invocations:
|
||||
|
||||
- ✅ `internal/migrations/schema.sql` - v1.0.0-rc.1 tables
|
||||
- `ucan_delegations` - CID-indexed delegation storage with envelope BLOB
|
||||
- `ucan_invocations` - CID-indexed invocation storage with execution tracking
|
||||
- `ucan_revocations` - Revocation records with reason and invocation CID
|
||||
- Updated `grants` table to use `delegation_cid` instead of `ucan_id`
|
||||
|
||||
- ✅ `internal/migrations/query.sql` - CID-based queries
|
||||
- Delegation CRUD: Create, Get by CID, List by DID/Issuer/Audience/Subject/Command
|
||||
- Invocation CRUD: Create, Get by CID, List by DID/Issuer/Command, Mark executed
|
||||
- Revocation: Create, Check revoked, Get revocation, List by revoker
|
||||
|
||||
- ✅ `internal/keybase/actions_delegation.go` - Delegation action handlers
|
||||
- StoreDelegation, GetDelegationByCID, GetDelegationEnvelope
|
||||
- ListDelegations, ListDelegationsByIssuer, ListDelegationsByAudience
|
||||
- ListDelegationsForCommand, IsDelegationRevoked, RevokeDelegation
|
||||
- DeleteDelegation, CleanExpiredDelegations
|
||||
|
||||
- ✅ `internal/keybase/actions_invocation.go` - Invocation action handlers
|
||||
- StoreInvocation, GetInvocationByCID, GetInvocationEnvelope
|
||||
- ListInvocations, ListInvocationsByCommand, ListPendingInvocations
|
||||
- MarkInvocationExecuted, CleanOldInvocations
|
||||
|
||||
- ✅ `main.go` - Updated exec handlers for v1.0.0-rc.1
|
||||
- `executeUCANAction` uses delegation methods (list, get, revoke, verify, cleanup)
|
||||
- `executeDelegationAction` uses CID-based methods (list by issuer/audience/command)
|
||||
- `validateUCAN` uses `IsDelegationRevoked` instead of old `IsUCANRevoked`
|
||||
|
||||
- ✅ Deleted old action files
|
||||
- `internal/keybase/actions_ucan.go` - Old JWT-based UCAN actions
|
||||
- `internal/keybase/actions_delegation.go` - Old ID-based delegation actions
|
||||
|
||||
### UCAN v1.0.0-rc.1 Core (January 2025)
|
||||
|
||||
The following was completed using `github.com/ucan-wg/go-ucan v1.1.0`:
|
||||
|
||||
- ✅ Type re-exports from go-ucan (Delegation, Invocation, Command, Policy)
|
||||
- ✅ Sonr command constants (/vault/*, /did/*, /dwn/*)
|
||||
- ✅ DelegationBuilder fluent API with Sonr-specific helpers
|
||||
- ✅ InvocationBuilder fluent API with Sonr-specific helpers
|
||||
- ✅ PolicyBuilder fluent API with all operators
|
||||
- ✅ Sonr policy helpers (VaultPolicy, DIDPolicy, ChainPolicy)
|
||||
- ✅ ValidationError types matching TypeScript definitions
|
||||
- ✅ Capability, ExecutionResult, and related types
|
||||
|
||||
### Deleted (Deprecated JWT-based)
|
||||
|
||||
- ✅ Deleted `jwt.go` - Old JWT token handling
|
||||
- ✅ Deleted `capability.go` - Old Attenuation/Resource/Capability model
|
||||
- ✅ Deleted `verifier.go` - Old JWT verification
|
||||
- ✅ Deleted `source.go` - Old JWT token creation
|
||||
- ✅ Deleted `internal/crypto/mpc/spec/` - Old MPC JWT integration
|
||||
- ✅ Removed `github.com/golang-jwt/jwt/v5` dependency
|
||||
|
||||
---
|
||||
|
||||
## Deprecated Items (Removed)
|
||||
|
||||
The following items from the previous TODO have been removed as they reference the **deprecated JWT-based UCAN format**:
|
||||
|
||||
- ~~Section 4.1 "Token Validation" - JWT parsing~~ -> Replaced by go-ucan validation
|
||||
- ~~Section 4.2 "Capability Verification" - `can`/`with` format~~ -> Replaced by policy evaluation
|
||||
- ~~Section 4.3 "Proof Chain Validation" - JWT proof strings~~ -> Replaced by CID-based chain
|
||||
- ~~Section 3.2 "UCAN Token Actions" - Old format~~ -> Replaced by v1.0.0-rc.1 actions (4.2)
|
||||
- ~~Section 3.3 "Delegation Actions" - Old delegation model~~ -> Merged into Section 1 and 4.2
|
||||
|
||||
The old capability model (`Attenuation`, `Resource`, `Capability` interfaces) is replaced by:
|
||||
|
||||
- `sub` (DID) - Subject of the capability
|
||||
- `cmd` (Command) - Action being delegated
|
||||
- `pol` (Policy) - Constraints on invocation arguments
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,6 +10,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"enclave/internal/crypto/bip44"
|
||||
"enclave/internal/crypto/mpc"
|
||||
"enclave/internal/keybase"
|
||||
"enclave/internal/state"
|
||||
"enclave/internal/types"
|
||||
@@ -48,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 {
|
||||
@@ -67,32 +71,32 @@ func generate() int32 {
|
||||
return 1
|
||||
}
|
||||
|
||||
did, err := initializeDatabase(credentialBytes)
|
||||
result, err := initializeWithMPC(credentialBytes)
|
||||
if err != nil {
|
||||
pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err))
|
||||
pdk.SetError(fmt.Errorf("generate: %w", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
state.SetInitialized(true)
|
||||
state.SetDID(did)
|
||||
state.SetDID(result.DID)
|
||||
|
||||
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", did))
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s with enclave %s", result.DID, result.EnclaveID))
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -238,20 +242,100 @@ func query() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func initializeDatabase(credentialBytes []byte) (string, error) {
|
||||
type initResult struct {
|
||||
DID string
|
||||
EnclaveID string
|
||||
PublicKey string
|
||||
Accounts []types.AccountInfo
|
||||
}
|
||||
|
||||
func initializeWithMPC(credentialBytes []byte) (*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 DID: %w", err)
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records")
|
||||
return did, nil
|
||||
simpleEnc, err := mpc.NewSimpleEnclave()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate enclave: %w", err)
|
||||
}
|
||||
|
||||
enclaveID := fmt.Sprintf("enc_%x", credentialBytes[:8])
|
||||
|
||||
am, err := keybase.NewActionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action manager: %w", err)
|
||||
}
|
||||
|
||||
enc, err := am.CreateEnclave(ctx, keybase.NewEnclaveInput{
|
||||
EnclaveID: enclaveID,
|
||||
PublicKeyHex: simpleEnc.PubKeyHex(),
|
||||
PublicKey: simpleEnc.PubKeyBytes(),
|
||||
ValShare: simpleEnc.GetShare1(),
|
||||
UserShare: simpleEnc.GetShare2(),
|
||||
Nonce: simpleEnc.GetNonce(),
|
||||
Curve: string(simpleEnc.GetCurve()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store enclave: %w", err)
|
||||
}
|
||||
|
||||
accounts, err := createDefaultAccounts(ctx, am, enc.ID, simpleEnc.PubKeyBytes())
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogWarn, fmt.Sprintf("createDefaultAccounts: %s", err))
|
||||
accounts = []types.AccountInfo{}
|
||||
}
|
||||
|
||||
return &initResult{
|
||||
DID: did,
|
||||
EnclaveID: enclaveID,
|
||||
PublicKey: simpleEnc.PubKeyHex(),
|
||||
Accounts: accounts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createDefaultAccounts(ctx context.Context, am *keybase.ActionManager, enclaveID int64, pubKeyBytes []byte) ([]types.AccountInfo, error) {
|
||||
chains := []string{"sonr", "ethereum", "bitcoin"}
|
||||
derivedAccounts, err := bip44.DeriveAccounts(pubKeyBytes, chains)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive accounts: %w", err)
|
||||
}
|
||||
|
||||
accounts := make([]types.AccountInfo, 0, len(derivedAccounts))
|
||||
for i, derived := range derivedAccounts {
|
||||
isDefault := int64(0)
|
||||
if i == 0 {
|
||||
isDefault = 1
|
||||
}
|
||||
|
||||
acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{
|
||||
EnclaveID: enclaveID,
|
||||
Address: derived.Address,
|
||||
ChainID: derived.ChainID,
|
||||
CoinType: int64(derived.CoinType),
|
||||
AccountIndex: int64(derived.AccountIndex),
|
||||
AddressIndex: int64(derived.AddressIndex),
|
||||
Label: derived.ChainID,
|
||||
IsDefault: isDefault,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
accounts = append(accounts, types.AccountInfo{
|
||||
Address: acc.Address,
|
||||
ChainID: acc.ChainID,
|
||||
CoinType: acc.CoinType,
|
||||
})
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func serializeDatabase() ([]byte, error) {
|
||||
@@ -318,56 +402,124 @@ func validateUCAN(token string, params *types.FilterParams) error {
|
||||
return errors.New("token is required")
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return errors.New("invalid token format: expected JWT with 3 parts")
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid token payload: %w", err)
|
||||
}
|
||||
|
||||
var claims map[string]any
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return fmt.Errorf("invalid token claims: %w", err)
|
||||
}
|
||||
|
||||
if exp, ok := claims["exp"].(float64); ok {
|
||||
if int64(exp) < currentUnixTime() {
|
||||
return errors.New("token has expired")
|
||||
}
|
||||
}
|
||||
|
||||
if nbf, ok := claims["nbf"].(float64); ok {
|
||||
if int64(nbf) > currentUnixTime() {
|
||||
return errors.New("token is not yet valid")
|
||||
}
|
||||
}
|
||||
|
||||
if aud, ok := claims["aud"].(string); ok {
|
||||
currentDID := state.GetDID()
|
||||
if currentDID != "" && aud != currentDID {
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: audience mismatch, expected %s got %s", currentDID, aud))
|
||||
}
|
||||
}
|
||||
|
||||
if att, ok := claims["att"].([]any); ok {
|
||||
if !checkAttenuations(att, params.Resource, params.Action) {
|
||||
return fmt.Errorf("token does not grant capability for %s:%s", params.Resource, params.Action)
|
||||
}
|
||||
}
|
||||
|
||||
am, err := keybase.NewActionManager()
|
||||
if err == nil {
|
||||
if cid, ok := claims["cid"].(string); ok {
|
||||
ctx := context.Background()
|
||||
revoked, err := am.IsDelegationRevoked(ctx, cid)
|
||||
if err == nil && revoked {
|
||||
return errors.New("token has been revoked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action))
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeAction(params *types.FilterParams) (json.RawMessage, error) {
|
||||
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 currentUnixTime() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) {
|
||||
am, err := keybase.NewActionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action manager: %w", err)
|
||||
func checkAttenuations(attenuations []any, resource, action string) bool {
|
||||
for _, att := range attenuations {
|
||||
attMap, ok := att.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
with, ok := attMap["with"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch params.Action {
|
||||
case "list":
|
||||
accounts, err := am.ListAccounts(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list accounts: %w", err)
|
||||
if !matchResource(with, resource) {
|
||||
continue
|
||||
}
|
||||
return json.Marshal(accounts)
|
||||
case "get":
|
||||
if params.Subject == "" {
|
||||
return nil, errors.New("subject (address) required for get action")
|
||||
|
||||
can := attMap["can"]
|
||||
if canStr, ok := can.(string); ok {
|
||||
if canStr == "*" || canStr == action {
|
||||
return true
|
||||
}
|
||||
account, err := am.GetAccountByAddress(ctx, params.Subject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get account: %w", err)
|
||||
} else if canSlice, ok := can.([]any); ok {
|
||||
for _, c := range canSlice {
|
||||
if cStr, ok := c.(string); ok {
|
||||
if cStr == "*" || cStr == action {
|
||||
return true
|
||||
}
|
||||
return json.Marshal(account)
|
||||
case "balances":
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchResource(pattern, resource string) bool {
|
||||
if pattern == resource {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(pattern, "/*") {
|
||||
prefix := strings.TrimSuffix(pattern, "/*")
|
||||
return strings.HasPrefix(resource, prefix)
|
||||
}
|
||||
|
||||
if strings.Contains(pattern, "://") {
|
||||
parts := strings.SplitN(pattern, "://", 2)
|
||||
if len(parts) == 2 && parts[1] == resource {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func executeAction(params *types.FilterParams) (json.RawMessage, error) {
|
||||
if params.Resource == "accounts" && params.Action == "balances" {
|
||||
return fetchAccountBalances(params.Subject)
|
||||
case "sign":
|
||||
return json.Marshal(map[string]string{"signature": "placeholder"})
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action for accounts: %s", params.Action)
|
||||
}
|
||||
return keybase.Exec(context.Background(), params.Resource, params.Action, params.Subject)
|
||||
}
|
||||
|
||||
func fetchAccountBalances(address string) (json.RawMessage, error) {
|
||||
@@ -404,95 +556,6 @@ func fetchAccountBalances(address string) (json.RawMessage, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) {
|
||||
am, err := keybase.NewActionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action manager: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch params.Action {
|
||||
case "list":
|
||||
credentials, err := am.ListCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list credentials: %w", err)
|
||||
}
|
||||
return json.Marshal(credentials)
|
||||
case "get":
|
||||
if params.Subject == "" {
|
||||
return nil, errors.New("subject (credential_id) required for get action")
|
||||
}
|
||||
credential, err := am.GetCredentialByID(ctx, params.Subject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get credential: %w", err)
|
||||
}
|
||||
return json.Marshal(credential)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action for credentials: %s", params.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func executeSessionAction(params *types.FilterParams) (json.RawMessage, error) {
|
||||
am, err := keybase.NewActionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action manager: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch params.Action {
|
||||
case "list":
|
||||
sessions, err := am.ListSessions(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sessions: %w", err)
|
||||
}
|
||||
return json.Marshal(sessions)
|
||||
case "revoke":
|
||||
if params.Subject == "" {
|
||||
return nil, errors.New("subject (session_id) required for revoke action")
|
||||
}
|
||||
if err := am.RevokeSession(ctx, params.Subject); err != nil {
|
||||
return nil, fmt.Errorf("revoke session: %w", err)
|
||||
}
|
||||
return json.Marshal(map[string]bool{"revoked": true})
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action for sessions: %s", params.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) {
|
||||
am, err := keybase.NewActionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action manager: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch params.Action {
|
||||
case "list":
|
||||
grants, err := am.ListGrants(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list grants: %w", err)
|
||||
}
|
||||
return json.Marshal(grants)
|
||||
case "revoke":
|
||||
if params.Subject == "" {
|
||||
return nil, errors.New("subject (grant_id) required for revoke action")
|
||||
}
|
||||
var grantID int64
|
||||
if _, err := fmt.Sscanf(params.Subject, "%d", &grantID); err != nil {
|
||||
return nil, fmt.Errorf("invalid grant_id: %w", err)
|
||||
}
|
||||
if err := am.RevokeGrant(ctx, grantID); err != nil {
|
||||
return nil, fmt.Errorf("revoke grant: %w", err)
|
||||
}
|
||||
return json.Marshal(map[string]bool{"revoked": true})
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action for grants: %s", params.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDID(did string) (*types.QueryOutput, error) {
|
||||
am, err := keybase.NewActionManager()
|
||||
if err != nil {
|
||||
@@ -3,108 +3,499 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Motr Enclave</title>
|
||||
<title>Motr Enclave Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0a0a;
|
||||
--surface: #141414;
|
||||
--surface-2: #1a1a1a;
|
||||
--border: #262626;
|
||||
--text: #e5e5e5;
|
||||
--text-muted: #737373;
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; padding: 2rem; }
|
||||
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #fff; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.card { background: #171717; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
|
||||
.card h2 { font-size: 0.875rem; color: #a3a3a3; margin-bottom: 0.5rem; font-weight: 500; }
|
||||
.status { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; margin-right: 0.5rem; }
|
||||
.status.ok { background: #14532d; color: #4ade80; }
|
||||
.status.err { background: #7f1d1d; color: #f87171; }
|
||||
.status.wait { background: #422006; color: #fbbf24; }
|
||||
button { background: #2563eb; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; margin-right: 0.5rem; margin-bottom: 0.5rem; }
|
||||
button:hover { background: #1d4ed8; }
|
||||
button:disabled { background: #374151; cursor: not-allowed; }
|
||||
input { width: 100%; background: #262626; border: 1px solid #404040; color: #fff; padding: 0.5rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem; margin-bottom: 0.5rem; }
|
||||
.log { background: #0a0a0a; border: 1px solid #262626; border-radius: 4px; padding: 0.5rem; font-family: monospace; font-size: 0.7rem; max-height: 150px; overflow-y: auto; white-space: pre-wrap; margin-top: 0.5rem; display: none; }
|
||||
.log.has-content { display: block; }
|
||||
.log-entry { padding: 0.125rem 0; border-bottom: 1px solid #1a1a1a; }
|
||||
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 1.5rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar .subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface-2);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.status-dot.ready { background: var(--success); }
|
||||
.status-dot.error { background: var(--error); }
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-section h3 {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-btn:hover { background: var(--surface-2); }
|
||||
.nav-btn.active { background: var(--primary); }
|
||||
|
||||
.main {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.main h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card h3 .badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: var(--surface-2);
|
||||
border-radius: 3px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover { background: var(--primary-hover); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover { background: var(--border); }
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.log {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.75rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log.visible { display: block; }
|
||||
|
||||
.log-entry {
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.log-entry:last-child { border-bottom: none; }
|
||||
.log-time { color: #525252; }
|
||||
|
||||
.log-time { color: var(--text-muted); margin-right: 0.5rem; }
|
||||
.log-info { color: #60a5fa; }
|
||||
.log-ok { color: #4ade80; }
|
||||
.log-err { color: #f87171; }
|
||||
.log-data { color: #a78bfa; display: block; margin-left: 1rem; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
|
||||
.clear-btn { background: #374151; padding: 0.25rem 0.5rem; font-size: 0.7rem; margin: 0; }
|
||||
.clear-btn:hover { background: #4b5563; }
|
||||
.status-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.log-data {
|
||||
color: #a78bfa;
|
||||
display: block;
|
||||
margin: 0.25rem 0 0 1.5rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.resource-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.resource-btn {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.resource-btn:hover { border-color: var(--primary); }
|
||||
.resource-btn.active { border-color: var(--primary); background: rgba(59, 130, 246, 0.1); }
|
||||
|
||||
.resource-btn .icon { font-size: 1.25rem; margin-bottom: 0.25rem; }
|
||||
.resource-btn .name { font-weight: 500; }
|
||||
|
||||
.action-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.action-pill {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.action-pill:hover { border-color: var(--primary); }
|
||||
.action-pill.active { background: var(--primary); border-color: var(--primary); }
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.8rem;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.result-panel.success { border-color: var(--success); }
|
||||
.result-panel.error { border-color: var(--error); }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
||||
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.sidebar {
|
||||
position: relative;
|
||||
height: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<h1>Motr Enclave</h1>
|
||||
<p class="subtitle">WASM Plugin Demo</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Status</h2>
|
||||
<div class="status-row">
|
||||
<span id="status" class="status wait">Loading...</span>
|
||||
<button onclick="runAllTests()" style="margin-left: 0.5rem;">Run All Tests</button>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span id="status-dot" class="status-dot"></span>
|
||||
<span id="status-text">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>ping(message)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('ping')">Clear</button>
|
||||
<nav>
|
||||
<div class="nav-section">
|
||||
<h3>Core Functions</h3>
|
||||
<button class="nav-btn active" onclick="showSection('setup')">Setup & Initialize</button>
|
||||
<button class="nav-btn" onclick="showSection('explorer')">Resource Explorer</button>
|
||||
<button class="nav-btn" onclick="showSection('query')">DID Query</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Quick Actions</h3>
|
||||
<button class="nav-btn" onclick="runAllTests()">Run All Tests</button>
|
||||
<button class="nav-btn" onclick="resetEnclave()">Reset Enclave</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Resources</h3>
|
||||
<button class="nav-btn" onclick="quickExec('accounts', 'list')">List Accounts</button>
|
||||
<button class="nav-btn" onclick="quickExec('enclaves', 'list')">List Enclaves</button>
|
||||
<button class="nav-btn" onclick="quickExec('credentials', 'list')">List Credentials</button>
|
||||
<button class="nav-btn" onclick="quickExec('sessions', 'list')">List Sessions</button>
|
||||
<button class="nav-btn" onclick="quickExec('grants', 'list')">List Grants</button>
|
||||
<button class="nav-btn" onclick="quickExec('delegations', 'list')">List Delegations</button>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<section id="section-setup" class="tab-content active">
|
||||
<h2>Setup & Initialize</h2>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h3>ping() <span class="badge">health check</span></h3>
|
||||
<input type="text" class="input" id="ping-msg" value="hello" placeholder="Message">
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="testPing()">Send Ping</button>
|
||||
</div>
|
||||
<input type="text" id="ping-msg" value="hello from browser" placeholder="Message to echo">
|
||||
<button onclick="testPing()">Run</button>
|
||||
<div id="log-ping" class="log"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>generate(credential)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('generate')">Clear</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button onclick="testGenerate()">Create with WebAuthn</button>
|
||||
<button onclick="testGenerateMock()">Create with Mock</button>
|
||||
<h3>generate() <span class="badge">create wallet</span></h3>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="testGenerate()">WebAuthn Credential</button>
|
||||
<button class="btn btn-secondary" onclick="testGenerateMock()">Mock Credential</button>
|
||||
</div>
|
||||
<div id="log-generate" class="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>load(database)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('load')">Clear</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button onclick="testLoadFromBytes()">Load from Bytes</button>
|
||||
<h3>load() <span class="badge">restore wallet</span></h3>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="testLoadFromBytes()">Load from Memory</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="downloadDatabase()">Download DB</button>
|
||||
<label class="btn btn-secondary btn-sm">
|
||||
Upload DB
|
||||
<input type="file" id="db-upload" accept=".db,.sqlite" style="display:none" onchange="uploadDatabase(event)">
|
||||
</label>
|
||||
</div>
|
||||
<div id="log-load" class="log"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section id="section-explorer" class="tab-content">
|
||||
<h2>Resource Explorer</h2>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>exec(filter)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('exec')">Clear</button>
|
||||
<h3>Select Resource</h3>
|
||||
<div class="resource-grid" id="resource-grid">
|
||||
<button class="resource-btn active" onclick="selectResource('accounts')">
|
||||
<div class="icon">💰</div>
|
||||
<div class="name">Accounts</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('enclaves')">
|
||||
<div class="icon">🔐</div>
|
||||
<div class="name">Enclaves</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('credentials')">
|
||||
<div class="icon">🔑</div>
|
||||
<div class="name">Credentials</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('sessions')">
|
||||
<div class="icon">📱</div>
|
||||
<div class="name">Sessions</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('grants')">
|
||||
<div class="icon">✅</div>
|
||||
<div class="name">Grants</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('delegations')">
|
||||
<div class="icon">📜</div>
|
||||
<div class="name">Delegations</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('verification_methods')">
|
||||
<div class="icon">🔏</div>
|
||||
<div class="name">Verification</div>
|
||||
</button>
|
||||
<button class="resource-btn" onclick="selectResource('services')">
|
||||
<div class="icon">🌐</div>
|
||||
<div class="name">Services</div>
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" id="filter" value="resource:accounts action:list" placeholder="resource:X action:Y">
|
||||
<div class="actions">
|
||||
<button onclick="testExec()">Run</button>
|
||||
<button onclick="setFilter('resource:accounts action:list')">Accounts</button>
|
||||
<button onclick="setFilter('resource:accounts action:balances subject:sonr1example')">Balances</button>
|
||||
<button onclick="setFilter('resource:credentials action:list')">Credentials</button>
|
||||
<button onclick="setFilter('resource:sessions action:list')">Sessions</button>
|
||||
</div>
|
||||
<div id="log-exec" class="log"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>query(did)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('query')">Clear</button>
|
||||
<h3>Actions for <span id="selected-resource">accounts</span></h3>
|
||||
<div class="action-pills" id="action-pills">
|
||||
<button class="action-pill active" onclick="selectAction('list')">list</button>
|
||||
<button class="action-pill" onclick="selectAction('get')">get</button>
|
||||
</div>
|
||||
<input type="text" id="did" placeholder="did:sonr:... (empty = current)">
|
||||
<button onclick="testQuery()">Run</button>
|
||||
<div id="log-query" class="log"></div>
|
||||
|
||||
<div id="subject-row" style="display: none;">
|
||||
<input type="text" class="input" id="subject-input" placeholder="Subject (address, id, etc.)">
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="executeAction()">Execute</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="clearResult()">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Result</h3>
|
||||
<div id="exec-result" class="result-panel">
|
||||
<span class="empty-state">Execute an action to see results</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="section-query" class="tab-content">
|
||||
<h2>DID Query</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>query() <span class="badge">resolve DID document</span></h3>
|
||||
<input type="text" class="input" id="did-input" placeholder="did:sonr:... (empty = current DID)">
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="testQuery()">Resolve DID</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>DID Document</h3>
|
||||
<div id="query-result" class="result-panel">
|
||||
<span class="empty-state">Query a DID to see the document</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./main.js"></script>
|
||||
|
||||
327
example/main.js
327
example/main.js
@@ -2,8 +2,22 @@ import { createEnclave } from '../dist/enclave.js';
|
||||
|
||||
let enclave = null;
|
||||
let lastDatabase = null;
|
||||
let currentResource = 'accounts';
|
||||
let currentAction = 'list';
|
||||
|
||||
const LogLevel = { INFO: 'info', OK: 'ok', ERR: 'err', DATA: 'data' };
|
||||
const RESOURCE_ACTIONS = {
|
||||
accounts: ['list', 'get', 'sign'],
|
||||
enclaves: ['list', 'get', 'sign', 'rotate', 'archive', 'delete'],
|
||||
credentials: ['list', 'get'],
|
||||
sessions: ['list', 'revoke'],
|
||||
grants: ['list', 'revoke'],
|
||||
delegations: ['list', 'list_received', 'list_command', 'get', 'revoke', 'verify', 'cleanup'],
|
||||
ucans: ['list', 'get', 'revoke', 'verify', 'cleanup'],
|
||||
verification_methods: ['list', 'get', 'delete'],
|
||||
services: ['list', 'get', 'get_by_id'],
|
||||
};
|
||||
|
||||
const ACTIONS_REQUIRING_SUBJECT = ['get', 'revoke', 'delete', 'verify', 'rotate', 'archive', 'list_received', 'list_command', 'get_by_id', 'sign'];
|
||||
|
||||
function log(card, level, message, data = null) {
|
||||
const el = document.getElementById(`log-${card}`);
|
||||
@@ -14,22 +28,30 @@ function log(card, level, message, data = null) {
|
||||
let entry = `<div class="log-entry"><span class="log-time">${time}</span> <span class="log-${level}">${message}</span>`;
|
||||
if (data !== null) {
|
||||
const json = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
entry += `<span class="log-data">${json}</span>`;
|
||||
entry += `<span class="log-data">${escapeHtml(json)}</span>`;
|
||||
}
|
||||
entry += '</div>';
|
||||
|
||||
el.innerHTML += entry;
|
||||
el.classList.add('has-content');
|
||||
el.classList.add('visible');
|
||||
el.scrollTop = el.scrollHeight;
|
||||
|
||||
console.log(`[${time}] [${card}] ${message}`, data ?? '');
|
||||
}
|
||||
|
||||
function setStatus(id, ok, message) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = message;
|
||||
el.className = `status ${ok ? 'ok' : ok === false ? 'err' : 'wait'}`;
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function setStatus(ready, message) {
|
||||
const dot = document.getElementById('status-dot');
|
||||
const text = document.getElementById('status-text');
|
||||
|
||||
if (dot) {
|
||||
dot.className = 'status-dot' + (ready === true ? ' ready' : ready === false ? ' error' : '');
|
||||
}
|
||||
if (text) {
|
||||
text.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +68,16 @@ async function createWebAuthnCredential() {
|
||||
const userId = crypto.getRandomValues(new Uint8Array(16));
|
||||
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
||||
|
||||
const publicKeyCredentialCreationOptions = {
|
||||
const options = {
|
||||
challenge,
|
||||
rp: {
|
||||
name: "Motr Enclave",
|
||||
name: "Motr Enclave Demo",
|
||||
id: window.location.hostname,
|
||||
},
|
||||
user: {
|
||||
id: userId,
|
||||
name: `user-${Date.now()}@motr.local`,
|
||||
displayName: "Motr User",
|
||||
displayName: "Motr Demo User",
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ alg: -7, type: "public-key" },
|
||||
@@ -70,9 +92,7 @@ async function createWebAuthnCredential() {
|
||||
attestation: "none",
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: publicKeyCredentialCreationOptions,
|
||||
});
|
||||
const credential = await navigator.credentials.create({ publicKey: options });
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
@@ -87,60 +107,71 @@ async function createWebAuthnCredential() {
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
log('generate', LogLevel.INFO, 'Loading enclave.wasm...');
|
||||
|
||||
setStatus(null, 'Loading...');
|
||||
enclave = await createEnclave('./enclave.wasm', { debug: true });
|
||||
|
||||
setStatus('status', true, 'Ready');
|
||||
log('generate', LogLevel.OK, 'Plugin loaded');
|
||||
setStatus(true, 'Ready');
|
||||
log('generate', 'ok', 'Plugin loaded successfully');
|
||||
} catch (err) {
|
||||
setStatus('status', false, 'Failed');
|
||||
log('generate', LogLevel.ERR, `Load failed: ${err?.message || String(err)}`);
|
||||
setStatus(false, 'Failed');
|
||||
log('generate', 'err', `Load failed: ${err?.message || String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.testPing = async function() {
|
||||
if (!enclave) return log('ping', LogLevel.ERR, 'Plugin not loaded');
|
||||
window.showSection = function(section) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
|
||||
|
||||
const message = document.getElementById('ping-msg').value || 'hello';
|
||||
log('ping', LogLevel.INFO, `Sending: "${message}"`);
|
||||
const sectionEl = document.getElementById(`section-${section}`);
|
||||
if (sectionEl) sectionEl.classList.add('active');
|
||||
|
||||
event?.target?.classList.add('active');
|
||||
};
|
||||
|
||||
window.testPing = async function() {
|
||||
if (!enclave) return log('ping', 'err', 'Plugin not loaded');
|
||||
|
||||
const message = document.getElementById('ping-msg')?.value || 'hello';
|
||||
log('ping', 'info', `Sending: "${message}"`);
|
||||
|
||||
try {
|
||||
const result = await enclave.ping(message);
|
||||
if (result.success) {
|
||||
log('ping', LogLevel.OK, `Response: "${result.echo}"`, result);
|
||||
log('ping', 'ok', `Response: "${result.echo}"`, result);
|
||||
} else {
|
||||
log('ping', LogLevel.ERR, result.message, result);
|
||||
log('ping', 'err', result.message, result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log('ping', LogLevel.ERR, err?.message || String(err));
|
||||
log('ping', 'err', err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testGenerate = async function() {
|
||||
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
|
||||
if (!enclave) return log('generate', 'err', 'Plugin not loaded');
|
||||
|
||||
if (!window.PublicKeyCredential) {
|
||||
log('generate', LogLevel.ERR, 'WebAuthn not supported in this browser');
|
||||
log('generate', 'err', 'WebAuthn not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log('generate', LogLevel.INFO, 'Requesting WebAuthn credential...');
|
||||
|
||||
log('generate', 'info', 'Requesting WebAuthn credential...');
|
||||
const credential = await createWebAuthnCredential();
|
||||
log('generate', LogLevel.OK, `Credential created: ${credential.id.slice(0, 20)}...`);
|
||||
log('generate', 'ok', `Credential created: ${credential.id.slice(0, 20)}...`);
|
||||
|
||||
const credentialJson = JSON.stringify(credential);
|
||||
const credentialBase64 = btoa(credentialJson);
|
||||
const credentialBase64 = btoa(JSON.stringify(credential));
|
||||
|
||||
log('generate', LogLevel.INFO, 'Calling enclave.generate()...');
|
||||
log('generate', 'info', 'Calling enclave.generate()...');
|
||||
const result = await enclave.generate(credentialBase64);
|
||||
|
||||
const logData = { did: result.did, dbSize: result.database?.length };
|
||||
log('generate', LogLevel.OK, `DID created: ${result.did}`, logData);
|
||||
log('generate', 'ok', `DID created: ${result.did}`, {
|
||||
did: result.did,
|
||||
enclaveId: result.enclave_id,
|
||||
publicKey: result.public_key?.slice(0, 20) + '...',
|
||||
accounts: result.accounts?.length ?? 0,
|
||||
dbSize: result.database?.length ?? 0,
|
||||
});
|
||||
|
||||
if (result.database) {
|
||||
lastDatabase = result.database;
|
||||
@@ -148,16 +179,16 @@ window.testGenerate = async function() {
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') {
|
||||
log('generate', LogLevel.ERR, 'User cancelled or WebAuthn not allowed');
|
||||
log('generate', 'err', 'User cancelled or WebAuthn not allowed');
|
||||
} else {
|
||||
log('generate', LogLevel.ERR, err?.message || String(err));
|
||||
log('generate', 'err', err?.message || String(err));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testGenerateMock = async function() {
|
||||
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
|
||||
if (!enclave) return log('generate', 'err', 'Plugin not loaded');
|
||||
|
||||
const mockCredential = btoa(JSON.stringify({
|
||||
id: `mock-${Date.now()}`,
|
||||
@@ -169,111 +200,247 @@ window.testGenerateMock = async function() {
|
||||
},
|
||||
}));
|
||||
|
||||
log('generate', LogLevel.INFO, 'Using mock credential...');
|
||||
log('generate', 'info', 'Using mock credential...');
|
||||
|
||||
try {
|
||||
const result = await enclave.generate(mockCredential);
|
||||
|
||||
const logData = { did: result.did, dbSize: result.database?.length };
|
||||
log('generate', LogLevel.OK, `DID created: ${result.did}`, logData);
|
||||
log('generate', 'ok', `DID created: ${result.did}`, {
|
||||
did: result.did,
|
||||
enclaveId: result.enclave_id,
|
||||
publicKey: result.public_key?.slice(0, 20) + '...',
|
||||
accounts: result.accounts?.length ?? 0,
|
||||
dbSize: result.database?.length ?? 0,
|
||||
});
|
||||
|
||||
if (result.database) {
|
||||
lastDatabase = result.database;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log('generate', LogLevel.ERR, err?.message || String(err));
|
||||
log('generate', 'err', err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testLoadFromBytes = async function() {
|
||||
if (!enclave) return log('load', LogLevel.ERR, 'Plugin not loaded');
|
||||
if (!enclave) return log('load', 'err', 'Plugin not loaded');
|
||||
|
||||
if (!lastDatabase) {
|
||||
return log('load', LogLevel.ERR, 'No database in memory - run generate first');
|
||||
return log('load', 'err', 'No database in memory - run generate first');
|
||||
}
|
||||
|
||||
log('load', LogLevel.INFO, `Loading from bytes (${lastDatabase.length} bytes)...`);
|
||||
log('load', 'info', `Loading from bytes (${lastDatabase.length} bytes)...`);
|
||||
|
||||
try {
|
||||
const result = await enclave.load(new Uint8Array(lastDatabase));
|
||||
|
||||
if (result.success) {
|
||||
log('load', LogLevel.OK, `Loaded DID: ${result.did}`, result);
|
||||
log('load', 'ok', `Loaded DID: ${result.did}`, result);
|
||||
} else {
|
||||
log('load', LogLevel.ERR, result.error, result);
|
||||
log('load', 'err', result.error, result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log('load', LogLevel.ERR, err?.message || String(err));
|
||||
log('load', 'err', err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testExec = async function() {
|
||||
if (!enclave) return log('exec', LogLevel.ERR, 'Plugin not loaded');
|
||||
window.downloadDatabase = function() {
|
||||
if (!lastDatabase) {
|
||||
alert('No database in memory. Run generate first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = document.getElementById('filter').value;
|
||||
if (!filter) return log('exec', LogLevel.ERR, 'Filter required');
|
||||
const blob = new Blob([new Uint8Array(lastDatabase)], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `enclave-${Date.now()}.db`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
log('exec', LogLevel.INFO, `Executing: ${filter}`);
|
||||
log('load', 'ok', `Downloaded database (${lastDatabase.length} bytes)`);
|
||||
};
|
||||
|
||||
window.uploadDatabase = async function(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
lastDatabase = Array.from(new Uint8Array(buffer));
|
||||
|
||||
log('load', 'info', `Uploaded ${file.name} (${lastDatabase.length} bytes)`);
|
||||
|
||||
if (enclave) {
|
||||
await testLoadFromBytes();
|
||||
}
|
||||
};
|
||||
|
||||
window.selectResource = function(resource) {
|
||||
currentResource = resource;
|
||||
currentAction = 'list';
|
||||
|
||||
document.querySelectorAll('.resource-btn').forEach(el => el.classList.remove('active'));
|
||||
event?.target?.closest('.resource-btn')?.classList.add('active');
|
||||
|
||||
document.getElementById('selected-resource').textContent = resource;
|
||||
|
||||
const actions = RESOURCE_ACTIONS[resource] || ['list'];
|
||||
const pillsContainer = document.getElementById('action-pills');
|
||||
pillsContainer.innerHTML = actions.map((action, i) =>
|
||||
`<button class="action-pill${i === 0 ? ' active' : ''}" onclick="selectAction('${action}')">${action}</button>`
|
||||
).join('');
|
||||
|
||||
updateSubjectRow();
|
||||
};
|
||||
|
||||
window.selectAction = function(action) {
|
||||
currentAction = action;
|
||||
|
||||
document.querySelectorAll('.action-pill').forEach(el => el.classList.remove('active'));
|
||||
event?.target?.classList.add('active');
|
||||
|
||||
updateSubjectRow();
|
||||
};
|
||||
|
||||
function updateSubjectRow() {
|
||||
const subjectRow = document.getElementById('subject-row');
|
||||
const subjectInput = document.getElementById('subject-input');
|
||||
|
||||
if (ACTIONS_REQUIRING_SUBJECT.includes(currentAction)) {
|
||||
subjectRow.style.display = 'block';
|
||||
|
||||
const placeholders = {
|
||||
get: 'ID or address',
|
||||
revoke: 'ID to revoke',
|
||||
delete: 'ID to delete',
|
||||
verify: 'CID to verify',
|
||||
rotate: 'Enclave ID',
|
||||
archive: 'Enclave ID',
|
||||
list_received: 'Audience DID',
|
||||
list_command: 'Command (e.g., msg/send)',
|
||||
get_by_id: 'Service ID (number)',
|
||||
sign: currentResource === 'enclaves' ? 'enclave_id:data_to_sign' : 'Data to sign (text)',
|
||||
};
|
||||
|
||||
subjectInput.placeholder = placeholders[currentAction] || 'Subject';
|
||||
} else {
|
||||
subjectRow.style.display = 'none';
|
||||
subjectInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
window.executeAction = async function() {
|
||||
if (!enclave) {
|
||||
showResult('Plugin not loaded', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = document.getElementById('subject-input')?.value || '';
|
||||
|
||||
if (ACTIONS_REQUIRING_SUBJECT.includes(currentAction) && !subject) {
|
||||
showResult(`Subject is required for action: ${currentAction}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let filter = `resource:${currentResource} action:${currentAction}`;
|
||||
if (subject) {
|
||||
filter += ` subject:${subject}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await enclave.exec(filter);
|
||||
|
||||
if (result.success) {
|
||||
log('exec', LogLevel.OK, 'Success', result);
|
||||
showResult(JSON.stringify(result.result, null, 2), false);
|
||||
} else {
|
||||
log('exec', LogLevel.ERR, result.error, result);
|
||||
showResult(result.error, true);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log('exec', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
showResult(err?.message || String(err), true);
|
||||
}
|
||||
};
|
||||
|
||||
window.quickExec = async function(resource, action) {
|
||||
if (!enclave) {
|
||||
alert('Plugin not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
showSection('explorer');
|
||||
selectResource(resource);
|
||||
selectAction(action);
|
||||
await executeAction();
|
||||
};
|
||||
|
||||
function showResult(content, isError = false) {
|
||||
const resultEl = document.getElementById('exec-result');
|
||||
resultEl.textContent = content;
|
||||
resultEl.className = 'result-panel' + (isError ? ' error' : ' success');
|
||||
}
|
||||
|
||||
window.clearResult = function() {
|
||||
const resultEl = document.getElementById('exec-result');
|
||||
resultEl.innerHTML = '<span class="empty-state">Execute an action to see results</span>';
|
||||
resultEl.className = 'result-panel';
|
||||
};
|
||||
|
||||
window.testQuery = async function() {
|
||||
if (!enclave) return log('query', LogLevel.ERR, 'Plugin not loaded');
|
||||
if (!enclave) {
|
||||
showQueryResult('Plugin not loaded', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const did = document.getElementById('did').value;
|
||||
log('query', LogLevel.INFO, did ? `Querying: ${did}` : 'Querying current DID...');
|
||||
const did = document.getElementById('did-input')?.value || '';
|
||||
|
||||
try {
|
||||
const result = await enclave.query(did);
|
||||
log('query', LogLevel.OK, `Resolved: ${result.did}`, result);
|
||||
return result;
|
||||
showQueryResult(JSON.stringify(result, null, 2), false);
|
||||
} catch (err) {
|
||||
log('query', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
showQueryResult(err?.message || String(err), true);
|
||||
}
|
||||
};
|
||||
|
||||
window.setFilter = function(filter) {
|
||||
document.getElementById('filter').value = filter;
|
||||
};
|
||||
function showQueryResult(content, isError = false) {
|
||||
const resultEl = document.getElementById('query-result');
|
||||
resultEl.textContent = content;
|
||||
resultEl.className = 'result-panel' + (isError ? ' error' : ' success');
|
||||
}
|
||||
|
||||
window.clearCardLog = function(card) {
|
||||
const el = document.getElementById(`log-${card}`);
|
||||
if (el) {
|
||||
el.innerHTML = '';
|
||||
el.classList.remove('has-content');
|
||||
window.resetEnclave = async function() {
|
||||
if (!enclave) return;
|
||||
|
||||
try {
|
||||
await enclave.reset();
|
||||
lastDatabase = null;
|
||||
log('generate', 'info', 'Enclave state reset');
|
||||
clearResult();
|
||||
document.getElementById('query-result').innerHTML = '<span class="empty-state">Query a DID to see the document</span>';
|
||||
} catch (err) {
|
||||
log('generate', 'err', `Reset failed: ${err?.message || String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.runAllTests = async function() {
|
||||
log('ping', LogLevel.INFO, '=== Running all tests ===');
|
||||
log('generate', 'info', '=== Running all tests ===');
|
||||
|
||||
try {
|
||||
await testPing();
|
||||
await testGenerateMock();
|
||||
await testLoadFromBytes();
|
||||
await testExec();
|
||||
|
||||
showSection('explorer');
|
||||
await quickExec('accounts', 'list');
|
||||
await quickExec('enclaves', 'list');
|
||||
|
||||
showSection('query');
|
||||
await testQuery();
|
||||
log('query', LogLevel.OK, '=== All tests passed ===');
|
||||
|
||||
log('generate', 'ok', '=== All tests completed ===');
|
||||
} catch (err) {
|
||||
log('query', LogLevel.ERR, `Tests failed: ${err?.message || String(err)}`);
|
||||
log('generate', 'err', `Tests failed: ${err?.message || String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
41
go.mod
41
go.mod
@@ -2,11 +2,48 @@ module enclave
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
||||
require (
|
||||
code.sonr.org/go/did-it v1.0.0
|
||||
code.sonr.org/go/ucan v1.1.0
|
||||
github.com/extism/go-pdk v1.1.3
|
||||
github.com/ipfs/go-cid v0.5.0
|
||||
github.com/ipld/go-ipld-prime v0.21.0
|
||||
github.com/ncruces/go-sqlite3 v0.30.4
|
||||
github.com/sonr-io/crypto v1.0.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.30.4 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.3 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/bwesterb/go-ristretto v1.2.3 // indirect
|
||||
github.com/consensys/gnark-crypto v0.19.0 // indirect
|
||||
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
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.1.0 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
||||
github.com/ucan-wg/go-varsig v1.0.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
110
go.sum
110
go.sum
@@ -1,10 +1,120 @@
|
||||
code.sonr.org/go/did-it v1.0.0 h1:Wh8igUkD6cuf0Ul3gawi27z2/M1YfdnQ/mD9gBq/2EU=
|
||||
code.sonr.org/go/did-it v1.0.0/go.mod h1:PFK6ItvNyB2xbnVqipBbkN9BK1Sq+E2lf1YfOyCA0Og=
|
||||
code.sonr.org/go/ucan v1.1.0 h1:0VuJCGzDPbzcTrjBBgQAmLu+2ARp1eYeXiRl+2A86R8=
|
||||
code.sonr.org/go/ucan v1.1.0/go.mod h1:QUGrUW93T2yVPiSiADLxj1RNpTJ/9RHaKoHGMGEC63A=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y=
|
||||
github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/consensys/gnark-crypto v0.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA=
|
||||
github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
|
||||
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
|
||||
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=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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=
|
||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
|
||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI=
|
||||
github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI=
|
||||
github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA=
|
||||
github.com/ncruces/go-sqlite3 v0.30.4/go.mod h1:7WR20VSC5IZusKhUdiR9y1NsUqnZgqIYCmKKoMEYg68=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
||||
github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
github.com/sonr-io/crypto v1.0.1 h1:pTsWbdvs8I8zTMalfCK7/ecCvFkBw9VIb/bKKGwMWGw=
|
||||
github.com/sonr-io/crypto v1.0.1/go.mod h1:f6YZo/FfbUQEEN8TMPAeFI8BOljbDNrui3IXuIzCa/E=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/ucan-wg/go-varsig v1.0.0 h1:Hrc437Zg+B5Eoajg+qZQZI3Q3ocPyjlnp3/Bz9ZnlWw=
|
||||
github.com/ucan-wg/go-varsig v1.0.0/go.mod h1:Sakln6IPooDPH+ClQ0VvR09TuwUhHcfLqcPiPkMZGh0=
|
||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
|
||||
305
internal/codec/policy.json
Normal file
305
internal/codec/policy.json
Normal file
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ucan.xyz/schemas/policy.json",
|
||||
"title": "UCAN Policy Language",
|
||||
"description": "Policy statements and selectors for constraining invocation arguments",
|
||||
"$defs": {
|
||||
"Selector": {
|
||||
"title": "Policy Selector",
|
||||
"description": "jq-inspired selector for navigating IPLD data structures",
|
||||
"type": "string",
|
||||
"pattern": "^\\.(([a-zA-Z_][a-zA-Z0-9_]*)|(\\[[^\\]]+\\])|(\\[\\])|(\\[-?\\d+\\])|(\\[\\d*:\\d*\\]))?((\\.[a-zA-Z_][a-zA-Z0-9_]*)|(\\[[^\\]]+\\])|(\\[\\])|(\\[-?\\d+\\])|(\\[\\d*:\\d*\\]))*\\??$",
|
||||
"examples": [
|
||||
".",
|
||||
".foo",
|
||||
".bar[0]",
|
||||
".items[-1]",
|
||||
".data[2:5]",
|
||||
".optional?"
|
||||
]
|
||||
},
|
||||
"GlobPattern": {
|
||||
"title": "Glob Pattern",
|
||||
"description": "Pattern for 'like' operator. * = wildcard, \\* = literal",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"*@example.com",
|
||||
"prefix*suffix"
|
||||
]
|
||||
},
|
||||
"EqualityOperator": {
|
||||
"title": "Equality Operator",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"==",
|
||||
"!="
|
||||
]
|
||||
},
|
||||
"InequalityOperator": {
|
||||
"title": "Inequality Operator",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<="
|
||||
]
|
||||
},
|
||||
"EqualityStatement": {
|
||||
"title": "Equality Statement",
|
||||
"description": "Deep comparison: [operator, selector, value]",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"$ref": "#/$defs/EqualityOperator"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/Selector"
|
||||
},
|
||||
{}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"examples": [
|
||||
[
|
||||
"==",
|
||||
".status",
|
||||
"draft"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
".deleted",
|
||||
true
|
||||
]
|
||||
]
|
||||
},
|
||||
"InequalityStatement": {
|
||||
"title": "Inequality Statement",
|
||||
"description": "Numeric comparison: [operator, selector, number]",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"$ref": "#/$defs/InequalityOperator"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/Selector"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"examples": [
|
||||
[
|
||||
">",
|
||||
".age",
|
||||
18
|
||||
],
|
||||
[
|
||||
"<=",
|
||||
".price",
|
||||
100.50
|
||||
]
|
||||
]
|
||||
},
|
||||
"LikeStatement": {
|
||||
"title": "Like Statement",
|
||||
"description": "Glob pattern matching: ['like', selector, pattern]",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"const": "like"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/Selector"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/GlobPattern"
|
||||
}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"examples": [
|
||||
[
|
||||
"like",
|
||||
".email",
|
||||
"*@example.com"
|
||||
]
|
||||
]
|
||||
},
|
||||
"NotStatement": {
|
||||
"title": "Not Statement",
|
||||
"description": "Logical negation: ['not', statement]",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"const": "not"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PolicyStatement"
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"AndStatement": {
|
||||
"title": "And Statement",
|
||||
"description": "Logical AND: ['and', [statements...]]. Empty array = true",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"const": "and"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PolicyStatement"
|
||||
}
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"OrStatement": {
|
||||
"title": "Or Statement",
|
||||
"description": "Logical OR: ['or', [statements...]]. Empty array = true",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"const": "or"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PolicyStatement"
|
||||
}
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"AllStatement": {
|
||||
"title": "All Statement",
|
||||
"description": "Universal quantifier: ['all', selector, statement]",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"const": "all"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/Selector"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PolicyStatement"
|
||||
}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"examples": [
|
||||
[
|
||||
"all",
|
||||
".reviewers",
|
||||
[
|
||||
"like",
|
||||
".email",
|
||||
"*@example.com"
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"AnyStatement": {
|
||||
"title": "Any Statement",
|
||||
"description": "Existential quantifier: ['any', selector, statement]",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"const": "any"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/Selector"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PolicyStatement"
|
||||
}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"examples": [
|
||||
[
|
||||
"any",
|
||||
".tags",
|
||||
[
|
||||
"==",
|
||||
".",
|
||||
"urgent"
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"PolicyStatement": {
|
||||
"title": "Policy Statement",
|
||||
"description": "A single policy predicate expression",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/EqualityStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/InequalityStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/LikeStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/NotStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/AndStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/OrStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/AllStatement"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/AnyStatement"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Policy": {
|
||||
"title": "UCAN Policy",
|
||||
"description": "Array of statements forming implicit AND. Constrains invocation args.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PolicyStatement"
|
||||
},
|
||||
"examples": [
|
||||
[],
|
||||
[
|
||||
[
|
||||
"==",
|
||||
".from",
|
||||
"alice@example.com"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"==",
|
||||
".status",
|
||||
"draft"
|
||||
],
|
||||
[
|
||||
"all",
|
||||
".reviewer",
|
||||
[
|
||||
"like",
|
||||
".email",
|
||||
"*@example.com"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
118
internal/codec/primitives.json
Normal file
118
internal/codec/primitives.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ucan.xyz/schemas/primitives.json",
|
||||
"title": "UCAN Primitive Types",
|
||||
"description": "Core primitive types used across all UCAN specifications",
|
||||
"$defs": {
|
||||
"DID": {
|
||||
"title": "Decentralized Identifier",
|
||||
"description": "A W3C Decentralized Identifier (DID) string",
|
||||
"type": "string",
|
||||
"pattern": "^did:[a-z0-9]+:[a-zA-Z0-9._%-]+(:[a-zA-Z0-9._%-]+)*([/?#].*)?$",
|
||||
"examples": [
|
||||
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||
"did:web:example.com"
|
||||
]
|
||||
},
|
||||
"CID": {
|
||||
"title": "Content Identifier",
|
||||
"description": "IPLD CIDv1 with DAG-CBOR codec and SHA-256 multihash (base58btc)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"/": {
|
||||
"type": "string",
|
||||
"pattern": "^zdpu[a-km-zA-HJ-NP-Z1-9]+$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"/"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Bytes": {
|
||||
"title": "Binary Data",
|
||||
"description": "Binary data in DAG-JSON format (base64 encoded)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"/": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bytes": {
|
||||
"type": "string",
|
||||
"contentEncoding": "base64"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bytes"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"/"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Timestamp": {
|
||||
"title": "Unix Timestamp",
|
||||
"description": "Unix timestamp in seconds (53-bit integer for JS compatibility)",
|
||||
"type": "integer",
|
||||
"minimum": -9007199254740991,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"NullableTimestamp": {
|
||||
"title": "Nullable Timestamp",
|
||||
"description": "Timestamp or null for non-expiring tokens",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Timestamp"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"VarsigHeader": {
|
||||
"title": "Varsig Header",
|
||||
"description": "Variable signature header with algorithm metadata",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Bytes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Signature": {
|
||||
"title": "Cryptographic Signature",
|
||||
"description": "Raw signature bytes",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Bytes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Command": {
|
||||
"title": "UCAN Command",
|
||||
"description": "Slash-delimited path describing an action (lowercase, starts with /)",
|
||||
"type": "string",
|
||||
"pattern": "^/([a-z0-9_\\u00C0-\\u024F]+(/[a-z0-9_\\u00C0-\\u024F]+)*)?$",
|
||||
"examples": [
|
||||
"/",
|
||||
"/crud/create",
|
||||
"/msg/send",
|
||||
"/ucan/revoke"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"title": "Metadata",
|
||||
"description": "Arbitrary metadata map",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"Arguments": {
|
||||
"title": "Command Arguments",
|
||||
"description": "Map of command arguments",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
1160
internal/codec/ucan-schemas.json
Normal file
1160
internal/codec/ucan-schemas.json
Normal file
File diff suppressed because it is too large
Load Diff
278
internal/crypto/bip44/bip44.go
Normal file
278
internal/crypto/bip44/bip44.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// Package bip44 provides BIP44 address derivation using MPC public keys.
|
||||
package bip44
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/ripemd160"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
type CoinType uint32
|
||||
|
||||
const (
|
||||
CoinTypeBitcoin CoinType = 0
|
||||
CoinTypeEthereum CoinType = 60
|
||||
CoinTypeCosmos CoinType = 118
|
||||
CoinTypeSonr CoinType = 703
|
||||
)
|
||||
|
||||
type ChainConfig struct {
|
||||
CoinType CoinType
|
||||
Prefix string
|
||||
ChainID string
|
||||
}
|
||||
|
||||
var DefaultChains = map[string]ChainConfig{
|
||||
"bitcoin": {
|
||||
CoinType: CoinTypeBitcoin,
|
||||
Prefix: "bc1",
|
||||
ChainID: "bitcoin-mainnet",
|
||||
},
|
||||
"ethereum": {
|
||||
CoinType: CoinTypeEthereum,
|
||||
Prefix: "0x",
|
||||
ChainID: "1",
|
||||
},
|
||||
"sonr": {
|
||||
CoinType: CoinTypeSonr,
|
||||
Prefix: "snr",
|
||||
ChainID: "sonr-testnet-1",
|
||||
},
|
||||
"cosmos": {
|
||||
CoinType: CoinTypeCosmos,
|
||||
Prefix: "cosmos",
|
||||
ChainID: "cosmoshub-4",
|
||||
},
|
||||
}
|
||||
|
||||
func DeriveAddress(pubKeyBytes []byte, chain string) (string, error) {
|
||||
config, ok := DefaultChains[chain]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("bip44: unknown chain: %s", chain)
|
||||
}
|
||||
return DeriveAddressWithConfig(pubKeyBytes, config)
|
||||
}
|
||||
|
||||
func DeriveAddressWithConfig(pubKeyBytes []byte, config ChainConfig) (string, error) {
|
||||
if len(pubKeyBytes) == 0 {
|
||||
return "", fmt.Errorf("bip44: public key cannot be empty")
|
||||
}
|
||||
|
||||
switch config.CoinType {
|
||||
case CoinTypeBitcoin:
|
||||
return deriveBitcoinAddress(pubKeyBytes, config.Prefix)
|
||||
case CoinTypeEthereum:
|
||||
return deriveEthereumAddress(pubKeyBytes)
|
||||
case CoinTypeCosmos, CoinTypeSonr:
|
||||
return deriveCosmosAddress(pubKeyBytes, config.Prefix)
|
||||
default:
|
||||
return "", fmt.Errorf("bip44: unsupported coin type: %d", config.CoinType)
|
||||
}
|
||||
}
|
||||
|
||||
// deriveBitcoinAddress creates P2WPKH (native SegWit) address using SHA256+RIPEMD160
|
||||
func deriveBitcoinAddress(pubKeyBytes []byte, prefix string) (string, error) {
|
||||
compressed := ensureCompressed(pubKeyBytes)
|
||||
if compressed == nil {
|
||||
return "", fmt.Errorf("bip44: invalid public key length: %d", len(pubKeyBytes))
|
||||
}
|
||||
|
||||
sha256Hash := sha256.Sum256(compressed)
|
||||
ripemd := ripemd160.New()
|
||||
ripemd.Write(sha256Hash[:])
|
||||
pubKeyHash := ripemd.Sum(nil)
|
||||
|
||||
return bech32Encode(prefix, pubKeyHash)
|
||||
}
|
||||
|
||||
// deriveEthereumAddress creates address using Keccak256 of uncompressed pubkey
|
||||
func deriveEthereumAddress(pubKeyBytes []byte) (string, error) {
|
||||
var keyData []byte
|
||||
|
||||
switch len(pubKeyBytes) {
|
||||
case 65:
|
||||
keyData = pubKeyBytes[1:] // strip 0x04 prefix
|
||||
case 64:
|
||||
keyData = pubKeyBytes
|
||||
default:
|
||||
return "", fmt.Errorf("bip44: ethereum requires uncompressed key (64 or 65 bytes), got %d", len(pubKeyBytes))
|
||||
}
|
||||
|
||||
hash := sha3.NewLegacyKeccak256()
|
||||
hash.Write(keyData)
|
||||
hashBytes := hash.Sum(nil)
|
||||
|
||||
return "0x" + hex.EncodeToString(hashBytes[12:]), nil
|
||||
}
|
||||
|
||||
// deriveCosmosAddress creates bech32 address using SHA256+RIPEMD160
|
||||
func deriveCosmosAddress(pubKeyBytes []byte, prefix string) (string, error) {
|
||||
compressed := ensureCompressed(pubKeyBytes)
|
||||
if compressed == nil {
|
||||
return "", fmt.Errorf("bip44: invalid public key length: %d", len(pubKeyBytes))
|
||||
}
|
||||
|
||||
sha256Hash := sha256.Sum256(compressed)
|
||||
ripemd := ripemd160.New()
|
||||
ripemd.Write(sha256Hash[:])
|
||||
addressBytes := ripemd.Sum(nil)
|
||||
|
||||
return bech32Encode(prefix, addressBytes)
|
||||
}
|
||||
|
||||
func ensureCompressed(pubKeyBytes []byte) []byte {
|
||||
switch len(pubKeyBytes) {
|
||||
case 33:
|
||||
return pubKeyBytes
|
||||
case 65:
|
||||
return compressPublicKey(pubKeyBytes)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func compressPublicKey(uncompressed []byte) []byte {
|
||||
if len(uncompressed) != 65 {
|
||||
return uncompressed
|
||||
}
|
||||
|
||||
x := uncompressed[1:33]
|
||||
y := uncompressed[33:65]
|
||||
|
||||
compressed := make([]byte, 33)
|
||||
if y[31]&1 == 0 {
|
||||
compressed[0] = 0x02
|
||||
} else {
|
||||
compressed[0] = 0x03
|
||||
}
|
||||
copy(compressed[1:], x)
|
||||
|
||||
return compressed
|
||||
}
|
||||
|
||||
func bech32Encode(hrp string, data []byte) (string, error) {
|
||||
converted, err := convertBits(data, 8, 5, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bip44: convert bits: %w", err)
|
||||
}
|
||||
return encode(hrp, converted)
|
||||
}
|
||||
|
||||
func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) {
|
||||
acc := uint32(0)
|
||||
bits := uint8(0)
|
||||
maxv := uint32(1<<toBits) - 1
|
||||
result := make([]byte, 0, len(data)*int(fromBits)/int(toBits)+1)
|
||||
|
||||
for _, b := range data {
|
||||
acc = (acc << fromBits) | uint32(b)
|
||||
bits += fromBits
|
||||
for bits >= toBits {
|
||||
bits -= toBits
|
||||
result = append(result, byte((acc>>bits)&maxv))
|
||||
}
|
||||
}
|
||||
|
||||
if pad {
|
||||
if bits > 0 {
|
||||
result = append(result, byte((acc<<(toBits-bits))&maxv))
|
||||
}
|
||||
} else if bits >= fromBits {
|
||||
return nil, fmt.Errorf("illegal zero padding")
|
||||
} else if ((acc << (toBits - bits)) & maxv) != 0 {
|
||||
return nil, fmt.Errorf("non-zero padding")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
func encode(hrp string, data []byte) (string, error) {
|
||||
checksum := createChecksum(hrp, data)
|
||||
combined := append(data, checksum...)
|
||||
|
||||
result := make([]byte, len(hrp)+1+len(combined))
|
||||
copy(result, hrp)
|
||||
result[len(hrp)] = '1'
|
||||
|
||||
for i, b := range combined {
|
||||
result[len(hrp)+1+i] = charset[b]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func createChecksum(hrp string, data []byte) []byte {
|
||||
values := append(hrpExpand(hrp), data...)
|
||||
values = append(values, []byte{0, 0, 0, 0, 0, 0}...)
|
||||
polymod := polymod(values) ^ 1
|
||||
|
||||
checksum := make([]byte, 6)
|
||||
for i := 0; i < 6; i++ {
|
||||
checksum[i] = byte((polymod >> (5 * (5 - i))) & 31)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
func hrpExpand(hrp string) []byte {
|
||||
result := make([]byte, len(hrp)*2+1)
|
||||
for i, c := range hrp {
|
||||
result[i] = byte(c >> 5)
|
||||
result[i+len(hrp)+1] = byte(c & 31)
|
||||
}
|
||||
result[len(hrp)] = 0
|
||||
return result
|
||||
}
|
||||
|
||||
func polymod(values []byte) uint32 {
|
||||
gen := []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
||||
chk := uint32(1)
|
||||
for _, v := range values {
|
||||
top := chk >> 25
|
||||
chk = (chk&0x1ffffff)<<5 ^ uint32(v)
|
||||
for i := 0; i < 5; i++ {
|
||||
if (top>>i)&1 == 1 {
|
||||
chk ^= gen[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Address string
|
||||
ChainID string
|
||||
CoinType CoinType
|
||||
AccountIndex uint32
|
||||
AddressIndex uint32
|
||||
}
|
||||
|
||||
func DeriveAccounts(pubKeyBytes []byte, chains []string) ([]Account, error) {
|
||||
accounts := make([]Account, 0, len(chains))
|
||||
|
||||
for _, chain := range chains {
|
||||
config, ok := DefaultChains[chain]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
addr, err := DeriveAddressWithConfig(pubKeyBytes, config)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
accounts = append(accounts, Account{
|
||||
Address: addr,
|
||||
ChainID: config.ChainID,
|
||||
CoinType: config.CoinType,
|
||||
AccountIndex: 0,
|
||||
AddressIndex: 0,
|
||||
})
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
499
internal/crypto/mpc/README.md
Normal file
499
internal/crypto/mpc/README.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# MPC (Multi-Party Computation) Cryptographic Library
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A comprehensive Go implementation of Multi-Party Computation (MPC) primitives for secure distributed cryptography. This package provides threshold signing, encrypted key management, and secure keyshare operations for decentralized applications.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Threshold Cryptography** - 2-of-2 MPC key generation and signing
|
||||
- ✅ **Secure Enclaves** - Encrypted keyshare storage and management
|
||||
- ✅ **Multiple Curves** - Support for secp256k1, P-256, Ed25519, BLS12-381, and more
|
||||
- ✅ **Key Refresh** - Proactive security through keyshare rotation
|
||||
- ✅ **ECDSA Signing** - Distributed signature generation with SHA3-256
|
||||
- ✅ **Encrypted Export/Import** - Secure enclave serialization with AES-GCM
|
||||
- ✅ **UCAN Integration** - MPC-based JWT signing for User-Controlled Authorization Networks
|
||||
|
||||
## Architecture
|
||||
|
||||
The package is built around the concept of secure **Enclaves** that manage distributed keyshares:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ MPC Enclave │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Alice Share │ │ Bob Share │ ←── Threshold 2/2 │
|
||||
│ │ (Validator) │ │ (User) │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ • Distributed Key Generation (DKG) │
|
||||
│ • Threshold Signing (2-of-2) │
|
||||
│ • Key Refresh (Proactive Security) │
|
||||
│ • Encrypted Storage (AES-GCM) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
go get github.com/sonr-io/crypto/mpc
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### Creating a New MPC Enclave
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sonr-io/crypto/mpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Generate a new MPC enclave with distributed keyshares
|
||||
enclave, err := mpc.NewEnclave()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get the public key
|
||||
pubKeyHex := enclave.PubKeyHex()
|
||||
fmt.Printf("Public Key: %s\n", pubKeyHex)
|
||||
|
||||
// Verify the enclave is valid
|
||||
if enclave.IsValid() {
|
||||
fmt.Println("✅ Enclave successfully created!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Signing and Verification
|
||||
|
||||
```go
|
||||
// Sign data using distributed MPC protocol
|
||||
message := []byte("Hello, distributed world!")
|
||||
signature, err := enclave.Sign(message)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
isValid, err := enclave.Verify(message, signature)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Signature valid: %t\n", isValid)
|
||||
```
|
||||
|
||||
#### Secure Export and Import
|
||||
|
||||
```go
|
||||
// Export enclave with encryption
|
||||
secretKey := []byte("my-super-secret-key-32-bytes-long")
|
||||
encryptedData, err := enclave.Encrypt(secretKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Import from encrypted data
|
||||
restoredEnclave, err := mpc.ImportEnclave(
|
||||
mpc.WithEncryptedData(encryptedData, secretKey),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Restored public key: %s\n", restoredEnclave.PubKeyHex())
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Enclaves
|
||||
|
||||
An **Enclave** represents a secure MPC keyshare environment that manages distributed cryptographic operations:
|
||||
|
||||
```go
|
||||
type Enclave interface {
|
||||
// Key Management
|
||||
PubKeyHex() string // Get public key as hex string
|
||||
PubKeyBytes() []byte // Get public key as bytes
|
||||
IsValid() bool // Check if enclave has valid keyshares
|
||||
|
||||
// Cryptographic Operations
|
||||
Sign(data []byte) ([]byte, error) // Threshold signing
|
||||
Verify(data []byte, sig []byte) (bool, error) // Signature verification
|
||||
Refresh() (Enclave, error) // Proactive key refresh
|
||||
|
||||
// Secure Storage
|
||||
Encrypt(key []byte) ([]byte, error) // Export encrypted
|
||||
Decrypt(key []byte, data []byte) ([]byte, error) // Import encrypted
|
||||
|
||||
// Serialization
|
||||
Marshal() ([]byte, error) // JSON serialization
|
||||
Unmarshal(data []byte) error // JSON deserialization
|
||||
|
||||
// Data Access
|
||||
GetData() *EnclaveData // Access enclave internals
|
||||
GetEnclave() Enclave // Self-reference
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Party Computation Protocol
|
||||
|
||||
The package implements a 2-of-2 threshold scheme:
|
||||
|
||||
1. **Alice (Validator)** - Server-side keyshare
|
||||
2. **Bob (User)** - Client-side keyshare
|
||||
|
||||
Both parties must participate in:
|
||||
- **Distributed Key Generation (DKG)** - Creates shared public key
|
||||
- **Threshold Signing** - Generates valid signatures cooperatively
|
||||
- **Key Refresh** - Rotates keyshares while preserving public key
|
||||
|
||||
### Supported Curves
|
||||
|
||||
The package supports multiple elliptic curves:
|
||||
|
||||
```go
|
||||
type CurveName string
|
||||
|
||||
const (
|
||||
K256Name CurveName = "secp256k1" // Bitcoin/Ethereum
|
||||
P256Name CurveName = "P-256" // NIST P-256
|
||||
ED25519Name CurveName = "ed25519" // EdDSA
|
||||
BLS12381G1Name CurveName = "BLS12381G1" // BLS12-381 G1
|
||||
BLS12381G2Name CurveName = "BLS12381G2" // BLS12-381 G2
|
||||
// ... more curves supported
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Import Options
|
||||
|
||||
The package provides flexible import mechanisms:
|
||||
|
||||
```go
|
||||
// Import from initial keyshares (after DKG)
|
||||
enclave, err := mpc.ImportEnclave(
|
||||
mpc.WithInitialShares(validatorShare, userShare, mpc.K256Name),
|
||||
)
|
||||
|
||||
// Import from existing enclave data
|
||||
enclave, err := mpc.ImportEnclave(
|
||||
mpc.WithEnclaveData(enclaveData),
|
||||
)
|
||||
|
||||
// Import from encrypted backup
|
||||
enclave, err := mpc.ImportEnclave(
|
||||
mpc.WithEncryptedData(encryptedBytes, secretKey),
|
||||
)
|
||||
```
|
||||
|
||||
### Key Refresh for Proactive Security
|
||||
|
||||
```go
|
||||
// Refresh keyshares while keeping the same public key
|
||||
refreshedEnclave, err := enclave.Refresh()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Public key remains the same
|
||||
fmt.Printf("Original: %s\n", enclave.PubKeyHex())
|
||||
fmt.Printf("Refreshed: %s\n", refreshedEnclave.PubKeyHex())
|
||||
// Both should be identical!
|
||||
|
||||
// But the enclave now has fresh keyshares
|
||||
// This provides forward secrecy against key compromise
|
||||
```
|
||||
|
||||
### Standalone Verification
|
||||
|
||||
```go
|
||||
// Verify signatures without the full enclave
|
||||
pubKeyBytes := enclave.PubKeyBytes()
|
||||
isValid, err := mpc.VerifyWithPubKey(pubKeyBytes, message, signature)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
### UCAN Integration
|
||||
|
||||
The package includes MPC-based JWT signing for UCAN tokens:
|
||||
|
||||
```go
|
||||
import "github.com/sonr-io/crypto/mpc/spec"
|
||||
|
||||
// Create MPC-backed UCAN token source
|
||||
// (Implementation details in spec package)
|
||||
keyshareSource := spec.KeyshareSource{
|
||||
// ... MPC enclave integration
|
||||
}
|
||||
|
||||
// Use with UCAN token creation
|
||||
token, err := keyshareSource.NewOriginToken(
|
||||
"did:key:audience",
|
||||
attenuations,
|
||||
facts,
|
||||
notBefore,
|
||||
expires,
|
||||
)
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### Encryption
|
||||
|
||||
All encrypted operations use **AES-GCM** with **SHA3-256** key derivation:
|
||||
|
||||
```go
|
||||
// Secure key derivation
|
||||
func GetHashKey(key []byte) []byte {
|
||||
hash := sha3.New256()
|
||||
hash.Write(key)
|
||||
return hash.Sum(nil)[:32] // 256-bit key
|
||||
}
|
||||
```
|
||||
|
||||
### Threshold Security
|
||||
|
||||
- **2-of-2 threshold** - Both parties required for operations
|
||||
- **No single point of failure** - Neither party alone can sign
|
||||
- **Proactive refresh** - Regular keyshare rotation without changing public key
|
||||
- **Forward secrecy** - Old keyshares cannot be used after refresh
|
||||
|
||||
### Cryptographic Primitives
|
||||
|
||||
- **ECDSA Signing** with **SHA3-256** message hashing
|
||||
- **AES-GCM** encryption with 12-byte nonces
|
||||
- **Secure random nonce generation**
|
||||
- **Multiple curve support** for different use cases
|
||||
|
||||
## Public API Reference
|
||||
|
||||
### Core Functions
|
||||
|
||||
```go
|
||||
// Generate new MPC enclave
|
||||
func NewEnclave() (Enclave, error)
|
||||
|
||||
// Import enclave from various sources
|
||||
func ImportEnclave(options ...ImportOption) (Enclave, error)
|
||||
|
||||
// Execute distributed signing protocol
|
||||
func ExecuteSigning(signFuncVal SignFunc, signFuncUser SignFunc) ([]byte, error)
|
||||
|
||||
// Execute keyshare refresh protocol
|
||||
func ExecuteRefresh(refreshFuncVal RefreshFunc, refreshFuncUser RefreshFunc,
|
||||
curve CurveName) (Enclave, error)
|
||||
|
||||
// Standalone signature verification
|
||||
func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error)
|
||||
```
|
||||
|
||||
### Import Options
|
||||
|
||||
```go
|
||||
type ImportOption func(Options) Options
|
||||
|
||||
// Create from initial DKG results
|
||||
func WithInitialShares(valKeyshare Message, userKeyshare Message,
|
||||
curve CurveName) ImportOption
|
||||
|
||||
// Create from encrypted backup
|
||||
func WithEncryptedData(data []byte, key []byte) ImportOption
|
||||
|
||||
// Create from existing data structure
|
||||
func WithEnclaveData(data *EnclaveData) ImportOption
|
||||
```
|
||||
|
||||
### EnclaveData Structure
|
||||
|
||||
```go
|
||||
type EnclaveData struct {
|
||||
PubHex string `json:"pub_hex"` // Compressed public key (hex)
|
||||
PubBytes []byte `json:"pub_bytes"` // Uncompressed public key
|
||||
ValShare Message `json:"val_share"` // Alice (validator) keyshare
|
||||
UserShare Message `json:"user_share"`// Bob (user) keyshare
|
||||
Nonce []byte `json:"nonce"` // Encryption nonce
|
||||
Curve CurveName `json:"curve"` // Elliptic curve name
|
||||
}
|
||||
```
|
||||
|
||||
### Protocol Types
|
||||
|
||||
```go
|
||||
type Message *protocol.Message // MPC protocol message
|
||||
type Signature *curves.EcdsaSignature // ECDSA signature
|
||||
type RefreshFunc interface{ protocol.Iterator } // Key refresh protocol
|
||||
type SignFunc interface{ protocol.Iterator } // Signing protocol
|
||||
type Point curves.Point // Elliptic curve point
|
||||
```
|
||||
|
||||
### Utility Functions
|
||||
|
||||
```go
|
||||
// Cryptographic utilities
|
||||
func GetHashKey(key []byte) []byte
|
||||
func SerializeSignature(sig *curves.EcdsaSignature) ([]byte, error)
|
||||
func DeserializeSignature(sigBytes []byte) (*curves.EcdsaSignature, error)
|
||||
|
||||
// Key conversion utilities
|
||||
func GetECDSAPoint(pubKey []byte) (*curves.EcPoint, error)
|
||||
|
||||
// Protocol error handling
|
||||
func CheckIteratedErrors(aErr, bErr error) error
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The package provides comprehensive error handling:
|
||||
|
||||
```go
|
||||
// Common error patterns
|
||||
enclave, err := mpc.NewEnclave()
|
||||
if err != nil {
|
||||
// Handle DKG failure
|
||||
log.Fatalf("Failed to generate enclave: %v", err)
|
||||
}
|
||||
|
||||
signature, err := enclave.Sign(data)
|
||||
if err != nil {
|
||||
// Handle signing protocol failure
|
||||
log.Fatalf("Failed to sign: %v", err)
|
||||
}
|
||||
|
||||
// Validation errors
|
||||
if !enclave.IsValid() {
|
||||
log.Fatal("Enclave has invalid keyshares")
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- **Minimal footprint** - Only active keyshares kept in memory
|
||||
- **Efficient serialization** - JSON-based with compression
|
||||
- **Secure cleanup** - Sensitive data cleared after use
|
||||
|
||||
### Network Communication
|
||||
|
||||
- **Minimal rounds** - Optimized protocol with few message exchanges
|
||||
- **Small messages** - Compact protocol message format
|
||||
- **Stateless operations** - No persistent connections required
|
||||
|
||||
### Cryptographic Performance
|
||||
|
||||
- **Hardware acceleration** - Leverages optimized curve implementations
|
||||
- **Efficient hashing** - SHA3-256 with minimal overhead
|
||||
- **Fast verification** - Public key operations optimized
|
||||
|
||||
## Testing
|
||||
|
||||
The package includes comprehensive tests:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
go test -v ./crypto/mpc
|
||||
|
||||
# Run specific test suites
|
||||
go test -v ./crypto/mpc -run TestEnclaveData
|
||||
go test -v ./crypto/mpc -run TestKeyShareGeneration
|
||||
go test -v ./crypto/mpc -run TestEnclaveOperations
|
||||
|
||||
# Run with race detection
|
||||
go test -race ./crypto/mpc
|
||||
|
||||
# Generate coverage report
|
||||
go test -cover ./crypto/mpc
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Decentralized Identity
|
||||
|
||||
- **DID key management** - Secure distributed identity keys
|
||||
- **Threshold signing** - Multi-party authorization for identity operations
|
||||
- **Key recovery** - Distributed backup and restore mechanisms
|
||||
|
||||
### Cryptocurrency Wallets
|
||||
|
||||
- **Multi-signature wallets** - True threshold custody solutions
|
||||
- **Exchange security** - Hot wallet protection with distributed keys
|
||||
- **Institutional custody** - Compliance-friendly key management
|
||||
|
||||
### Blockchain Infrastructure
|
||||
|
||||
- **Validator signing** - Secure consensus participation
|
||||
- **Cross-chain bridges** - Multi-party custody of bridged assets
|
||||
- **DAO governance** - Distributed decision-making mechanisms
|
||||
|
||||
### Enterprise Applications
|
||||
|
||||
- **Document signing** - Distributed digital signatures
|
||||
- **API authentication** - Threshold-based service authentication
|
||||
- **Secure communication** - End-to-end encrypted messaging
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Core Cryptography**: `github.com/sonr-io/crypto/core/curves`
|
||||
- **Protocol Framework**: `github.com/sonr-io/crypto/core/protocol`
|
||||
- **Threshold ECDSA**: `github.com/sonr-io/crypto/tecdsa/dklsv1`
|
||||
- **UCAN Integration**: `github.com/sonr-io/crypto/ucan`
|
||||
- **Standard Crypto**: `golang.org/x/crypto/sha3`
|
||||
- **JWT Support**: `github.com/golang-jwt/jwt`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
The package is designed to protect against:
|
||||
|
||||
- **Key compromise** - Distributed keyshares prevent single points of failure
|
||||
- **Insider threats** - No single party can perform operations alone
|
||||
- **Network attacks** - Protocol messages are cryptographically protected
|
||||
- **Side-channel attacks** - Secure implementations of cryptographic primitives
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Regular key refresh** - Rotate keyshares periodically
|
||||
2. **Secure communication** - Use TLS for protocol message exchange
|
||||
3. **Access controls** - Implement proper authentication for MPC operations
|
||||
4. **Audit logging** - Log all cryptographic operations
|
||||
5. **Backup strategies** - Securely store encrypted enclave exports
|
||||
|
||||
### Limitations
|
||||
|
||||
- **2-of-2 threshold only** - Currently supports only 2-party protocols
|
||||
- **Network dependency** - Requires communication between parties
|
||||
- **No byzantine fault tolerance** - Assumes honest-but-curious adversaries
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please ensure:
|
||||
|
||||
1. **Security first** - All cryptographic code must be carefully reviewed
|
||||
2. **Comprehensive testing** - Include unit tests and integration tests
|
||||
3. **Documentation** - Document all public APIs and security assumptions
|
||||
4. **Performance** - Benchmark critical cryptographic operations
|
||||
5. **Compatibility** - Maintain backward compatibility with existing enclaves
|
||||
|
||||
## License
|
||||
|
||||
This project follows the same license as the main Sonr project.
|
||||
|
||||
---
|
||||
|
||||
**⚠️ Security Notice**: This is cryptographic software. While extensively tested, it should be used with appropriate security measures and understanding of the underlying protocols. For production deployments, consider additional security audits and operational security measures.
|
||||
11
internal/crypto/mpc/codec.go
Normal file
11
internal/crypto/mpc/codec.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package mpc
|
||||
|
||||
type CurveName string
|
||||
|
||||
const (
|
||||
K256Name CurveName = "secp256k1"
|
||||
P256Name CurveName = "P-256"
|
||||
ED25519Name CurveName = "ed25519"
|
||||
)
|
||||
|
||||
func (c CurveName) String() string { return string(c) }
|
||||
219
internal/crypto/mpc/simple.go
Normal file
219
internal/crypto/mpc/simple.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package mpc
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/sonr-io/crypto/core/curves"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
type SimpleEnclave struct {
|
||||
pubKey *ecdsa.PublicKey
|
||||
pubHex string
|
||||
pubBytes []byte
|
||||
share1 []byte
|
||||
share2 []byte
|
||||
nonce []byte
|
||||
curveName CurveName
|
||||
}
|
||||
|
||||
func NewSimpleEnclave() (*SimpleEnclave, error) {
|
||||
curve := curves.K256()
|
||||
ecCurve, err := curve.ToEllipticCurve()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get elliptic curve: %w", err)
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(ecCurve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
|
||||
share1, share2, err := splitSecret(privKey.D, ecCurve.Params().N)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split secret: %w", err)
|
||||
}
|
||||
|
||||
pubBytes := make([]byte, 65)
|
||||
pubBytes[0] = 0x04
|
||||
copy(pubBytes[1:33], padTo32(privKey.PublicKey.X.Bytes()))
|
||||
copy(pubBytes[33:65], padTo32(privKey.PublicKey.Y.Bytes()))
|
||||
|
||||
compressed := compressPubKey(pubBytes)
|
||||
|
||||
nonce := make([]byte, 12)
|
||||
rand.Read(nonce)
|
||||
|
||||
return &SimpleEnclave{
|
||||
pubKey: &privKey.PublicKey,
|
||||
pubHex: hex.EncodeToString(compressed),
|
||||
pubBytes: pubBytes,
|
||||
share1: share1,
|
||||
share2: share2,
|
||||
nonce: nonce,
|
||||
curveName: K256Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func splitSecret(secret *big.Int, order *big.Int) ([]byte, []byte, error) {
|
||||
share1Bytes := make([]byte, 32)
|
||||
_, err := rand.Read(share1Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
share1 := new(big.Int).SetBytes(share1Bytes)
|
||||
share1.Mod(share1, order)
|
||||
|
||||
share2 := new(big.Int).Sub(secret, share1)
|
||||
share2.Mod(share2, order)
|
||||
|
||||
return padTo32(share1.Bytes()), padTo32(share2.Bytes()), nil
|
||||
}
|
||||
|
||||
func combineShares(share1, share2 []byte, order *big.Int) *big.Int {
|
||||
s1 := new(big.Int).SetBytes(share1)
|
||||
s2 := new(big.Int).SetBytes(share2)
|
||||
result := new(big.Int).Add(s1, s2)
|
||||
return result.Mod(result, order)
|
||||
}
|
||||
|
||||
func (e *SimpleEnclave) Sign(data []byte) ([]byte, error) {
|
||||
curve := curves.K256()
|
||||
ecCurve, err := curve.ToEllipticCurve()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privKeyD := combineShares(e.share1, e.share2, ecCurve.Params().N)
|
||||
|
||||
privKey := &ecdsa.PrivateKey{
|
||||
PublicKey: *e.pubKey,
|
||||
D: privKeyD,
|
||||
}
|
||||
|
||||
hash := sha3.New256()
|
||||
hash.Write(data)
|
||||
digest := hash.Sum(nil)
|
||||
|
||||
r, s, err := ecdsa.Sign(rand.Reader, privKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, 64)
|
||||
copy(sig[0:32], padTo32(r.Bytes()))
|
||||
copy(sig[32:64], padTo32(s.Bytes()))
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func (e *SimpleEnclave) Verify(data []byte, sig []byte) (bool, error) {
|
||||
if len(sig) != 64 {
|
||||
return false, fmt.Errorf("invalid signature length: %d", len(sig))
|
||||
}
|
||||
|
||||
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(e.pubKey, digest, r, s), nil
|
||||
}
|
||||
|
||||
func (e *SimpleEnclave) PubKeyHex() string { return e.pubHex }
|
||||
func (e *SimpleEnclave) PubKeyBytes() []byte { return e.pubBytes }
|
||||
func (e *SimpleEnclave) IsValid() bool { return len(e.share1) > 0 && len(e.share2) > 0 }
|
||||
func (e *SimpleEnclave) GetShare1() []byte { return e.share1 }
|
||||
func (e *SimpleEnclave) GetShare2() []byte { return e.share2 }
|
||||
func (e *SimpleEnclave) GetNonce() []byte { return e.nonce }
|
||||
func (e *SimpleEnclave) GetCurve() CurveName { return e.curveName }
|
||||
|
||||
func (e *SimpleEnclave) Refresh() (*SimpleEnclave, error) {
|
||||
curve := curves.K256()
|
||||
ecCurve, err := curve.ToEllipticCurve()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privKeyD := combineShares(e.share1, e.share2, ecCurve.Params().N)
|
||||
newShare1, newShare2, err := splitSecret(privKeyD, ecCurve.Params().N)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newNonce := make([]byte, 12)
|
||||
rand.Read(newNonce)
|
||||
|
||||
return &SimpleEnclave{
|
||||
pubKey: e.pubKey,
|
||||
pubHex: e.pubHex,
|
||||
pubBytes: e.pubBytes,
|
||||
share1: newShare1,
|
||||
share2: newShare2,
|
||||
nonce: newNonce,
|
||||
curveName: e.curveName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ImportSimpleEnclave(pubBytes, share1, share2, nonce []byte, curveName CurveName) (*SimpleEnclave, error) {
|
||||
if len(pubBytes) != 65 {
|
||||
return nil, fmt.Errorf("invalid pubkey length: %d", len(pubBytes))
|
||||
}
|
||||
|
||||
curve := curves.K256()
|
||||
ecCurve, err := curve.ToEllipticCurve()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x := new(big.Int).SetBytes(pubBytes[1:33])
|
||||
y := new(big.Int).SetBytes(pubBytes[33:65])
|
||||
|
||||
pubKey := &ecdsa.PublicKey{
|
||||
Curve: ecCurve,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
|
||||
compressed := compressPubKey(pubBytes)
|
||||
|
||||
return &SimpleEnclave{
|
||||
pubKey: pubKey,
|
||||
pubHex: hex.EncodeToString(compressed),
|
||||
pubBytes: pubBytes,
|
||||
share1: share1,
|
||||
share2: share2,
|
||||
nonce: nonce,
|
||||
curveName: curveName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func padTo32(b []byte) []byte {
|
||||
if len(b) >= 32 {
|
||||
return b[:32]
|
||||
}
|
||||
padded := make([]byte, 32)
|
||||
copy(padded[32-len(b):], b)
|
||||
return padded
|
||||
}
|
||||
|
||||
func compressPubKey(uncompressed []byte) []byte {
|
||||
if len(uncompressed) != 65 {
|
||||
return uncompressed
|
||||
}
|
||||
|
||||
compressed := make([]byte, 33)
|
||||
if uncompressed[64]&1 == 0 {
|
||||
compressed[0] = 0x02
|
||||
} else {
|
||||
compressed[0] = 0x03
|
||||
}
|
||||
copy(compressed[1:], uncompressed[1:33])
|
||||
return compressed
|
||||
}
|
||||
107
internal/crypto/mpc/verify.go
Normal file
107
internal/crypto/mpc/verify.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package mpc
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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, 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
|
||||
}
|
||||
271
internal/crypto/ucan/delegation.go
Normal file
271
internal/crypto/ucan/delegation.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.sonr.org/go/did-it"
|
||||
"code.sonr.org/go/did-it/crypto"
|
||||
"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.
|
||||
// A delegation grants authority from issuer to audience to perform a command on a subject.
|
||||
type DelegationBuilder struct {
|
||||
issuer did.DID
|
||||
audience did.DID
|
||||
subject did.DID
|
||||
cmd command.Command
|
||||
pol policy.Policy
|
||||
opts []delegation.Option
|
||||
err error
|
||||
isPowerline bool
|
||||
}
|
||||
|
||||
// NewDelegationBuilder creates a builder for constructing UCAN delegations.
|
||||
func NewDelegationBuilder() *DelegationBuilder {
|
||||
return &DelegationBuilder{
|
||||
pol: policy.Policy{},
|
||||
opts: make([]delegation.Option, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Issuer sets the delegation issuer (the entity granting authority).
|
||||
func (b *DelegationBuilder) Issuer(iss did.DID) *DelegationBuilder {
|
||||
b.issuer = iss
|
||||
return b
|
||||
}
|
||||
|
||||
// IssuerString parses and sets the issuer from a DID string.
|
||||
func (b *DelegationBuilder) IssuerString(issStr string) *DelegationBuilder {
|
||||
iss, err := did.Parse(issStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid issuer DID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.issuer = iss
|
||||
return b
|
||||
}
|
||||
|
||||
// Audience sets the delegation audience (the entity receiving authority).
|
||||
func (b *DelegationBuilder) Audience(aud did.DID) *DelegationBuilder {
|
||||
b.audience = aud
|
||||
return b
|
||||
}
|
||||
|
||||
// AudienceString parses and sets the audience from a DID string.
|
||||
func (b *DelegationBuilder) AudienceString(audStr string) *DelegationBuilder {
|
||||
aud, err := did.Parse(audStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid audience DID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.audience = aud
|
||||
return b
|
||||
}
|
||||
|
||||
// Subject sets the delegation subject (the resource being delegated).
|
||||
// For root delegations, subject should equal issuer.
|
||||
// For powerline delegations, use Powerline() instead.
|
||||
func (b *DelegationBuilder) Subject(sub did.DID) *DelegationBuilder {
|
||||
b.subject = sub
|
||||
b.isPowerline = false
|
||||
return b
|
||||
}
|
||||
|
||||
// SubjectString parses and sets the subject from a DID string.
|
||||
func (b *DelegationBuilder) SubjectString(subStr string) *DelegationBuilder {
|
||||
sub, err := did.Parse(subStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid subject DID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.subject = sub
|
||||
b.isPowerline = false
|
||||
return b
|
||||
}
|
||||
|
||||
// Powerline creates a powerline delegation (subject = nil).
|
||||
// Powerline automatically delegates all future delegations regardless of subject.
|
||||
// Use with caution - this is a very powerful pattern.
|
||||
func (b *DelegationBuilder) Powerline() *DelegationBuilder {
|
||||
b.subject = nil
|
||||
b.isPowerline = true
|
||||
return b
|
||||
}
|
||||
|
||||
// AsRoot sets the subject equal to the issuer (root delegation).
|
||||
// Root delegations are typically used to create the initial grant of authority.
|
||||
func (b *DelegationBuilder) AsRoot() *DelegationBuilder {
|
||||
b.subject = b.issuer
|
||||
b.isPowerline = false
|
||||
return b
|
||||
}
|
||||
|
||||
// Command sets the command being delegated.
|
||||
func (b *DelegationBuilder) Command(cmd command.Command) *DelegationBuilder {
|
||||
b.cmd = cmd
|
||||
return b
|
||||
}
|
||||
|
||||
// CommandString parses and sets the command from a string.
|
||||
func (b *DelegationBuilder) CommandString(cmdStr string) *DelegationBuilder {
|
||||
cmd, err := command.Parse(cmdStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid command: %w", err)
|
||||
return b
|
||||
}
|
||||
b.cmd = cmd
|
||||
return b
|
||||
}
|
||||
|
||||
// Policy sets the policy constraints on the delegation.
|
||||
func (b *DelegationBuilder) Policy(pol policy.Policy) *DelegationBuilder {
|
||||
b.pol = pol
|
||||
return b
|
||||
}
|
||||
|
||||
// ExpiresAt sets when the delegation expires.
|
||||
func (b *DelegationBuilder) ExpiresAt(exp time.Time) *DelegationBuilder {
|
||||
b.opts = append(b.opts, delegation.WithExpiration(exp))
|
||||
return b
|
||||
}
|
||||
|
||||
// ExpiresIn sets the delegation to expire after a duration from now.
|
||||
func (b *DelegationBuilder) ExpiresIn(d time.Duration) *DelegationBuilder {
|
||||
b.opts = append(b.opts, delegation.WithExpirationIn(d))
|
||||
return b
|
||||
}
|
||||
|
||||
// NotBefore sets when the delegation becomes valid.
|
||||
func (b *DelegationBuilder) NotBefore(nbf time.Time) *DelegationBuilder {
|
||||
b.opts = append(b.opts, delegation.WithNotBefore(nbf))
|
||||
return b
|
||||
}
|
||||
|
||||
// NotBeforeIn sets the delegation to become valid after a duration from now.
|
||||
func (b *DelegationBuilder) NotBeforeIn(d time.Duration) *DelegationBuilder {
|
||||
b.opts = append(b.opts, delegation.WithNotBeforeIn(d))
|
||||
return b
|
||||
}
|
||||
|
||||
// Meta adds metadata to the delegation.
|
||||
func (b *DelegationBuilder) Meta(key string, value any) *DelegationBuilder {
|
||||
b.opts = append(b.opts, delegation.WithMeta(key, value))
|
||||
return b
|
||||
}
|
||||
|
||||
// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated.
|
||||
func (b *DelegationBuilder) Nonce(nonce []byte) *DelegationBuilder {
|
||||
b.opts = append(b.opts, delegation.WithNonce(nonce))
|
||||
return b
|
||||
}
|
||||
|
||||
// Build creates the delegation token.
|
||||
func (b *DelegationBuilder) Build() (*delegation.Token, error) {
|
||||
if b.err != nil {
|
||||
return nil, b.err
|
||||
}
|
||||
|
||||
if b.issuer == nil {
|
||||
return nil, fmt.Errorf("issuer is required")
|
||||
}
|
||||
if b.audience == nil {
|
||||
return nil, fmt.Errorf("audience is required")
|
||||
}
|
||||
if b.cmd == "" {
|
||||
return nil, fmt.Errorf("command is required")
|
||||
}
|
||||
|
||||
if b.isPowerline {
|
||||
return delegation.Powerline(b.issuer, b.audience, b.cmd, b.pol, b.opts...)
|
||||
}
|
||||
|
||||
// If subject not set and not powerline, default to root delegation
|
||||
if b.subject == nil {
|
||||
b.subject = b.issuer
|
||||
}
|
||||
|
||||
return delegation.New(b.issuer, b.audience, b.cmd, b.pol, b.subject, b.opts...)
|
||||
}
|
||||
|
||||
// BuildSealed creates and signs the delegation, returning DAG-CBOR bytes and CID.
|
||||
func (b *DelegationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) {
|
||||
tkn, err := b.Build()
|
||||
if err != nil {
|
||||
return nil, cid.Cid{}, err
|
||||
}
|
||||
return tkn.ToSealed(privKey)
|
||||
}
|
||||
|
||||
// --- Sonr-specific Delegation Builders ---
|
||||
|
||||
// NewVaultDelegation creates a delegation for vault operations.
|
||||
// cmd should be one of: VaultRead, VaultWrite, VaultSign, VaultExport, VaultImport, VaultDelete, VaultAdmin, or Vault (all).
|
||||
func NewVaultDelegation(issuer, audience did.DID, cmd command.Command, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
pol := VaultPolicy(vaultCID)
|
||||
return delegation.Root(issuer, audience, cmd, pol, opts...)
|
||||
}
|
||||
|
||||
// NewVaultReadDelegation creates a delegation to read from a specific vault.
|
||||
func NewVaultReadDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewVaultDelegation(issuer, audience, VaultRead, vaultCID, opts...)
|
||||
}
|
||||
|
||||
// NewVaultWriteDelegation creates a delegation to write to a specific vault.
|
||||
func NewVaultWriteDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewVaultDelegation(issuer, audience, VaultWrite, vaultCID, opts...)
|
||||
}
|
||||
|
||||
// NewVaultSignDelegation creates a delegation to sign with keys in a specific vault.
|
||||
func NewVaultSignDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewVaultDelegation(issuer, audience, VaultSign, vaultCID, opts...)
|
||||
}
|
||||
|
||||
// NewVaultFullAccessDelegation creates a delegation for all vault operations on a specific vault.
|
||||
func NewVaultFullAccessDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewVaultDelegation(issuer, audience, Vault, vaultCID, opts...)
|
||||
}
|
||||
|
||||
// NewDIDDelegation creates a delegation for DID operations.
|
||||
// cmd should be one of: DIDCreate, DIDUpdate, DIDDeactivate, or DID (all).
|
||||
func NewDIDDelegation(issuer, audience did.DID, cmd command.Command, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
pol := DIDPolicy(didPattern)
|
||||
return delegation.Root(issuer, audience, cmd, pol, opts...)
|
||||
}
|
||||
|
||||
// NewDIDUpdateDelegation creates a delegation to update DIDs matching a pattern.
|
||||
func NewDIDUpdateDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewDIDDelegation(issuer, audience, DIDUpdate, didPattern, opts...)
|
||||
}
|
||||
|
||||
// NewDIDFullAccessDelegation creates a delegation for all DID operations on DIDs matching a pattern.
|
||||
func NewDIDFullAccessDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewDIDDelegation(issuer, audience, DID, didPattern, opts...)
|
||||
}
|
||||
|
||||
// NewDWNDelegation creates a delegation for DWN (Decentralized Web Node) operations.
|
||||
// cmd should be one of: DWNRecordsWrite, DWNRecordsRead, DWNRecordsDelete, or DWN (all).
|
||||
func NewDWNDelegation(issuer, audience did.DID, cmd command.Command, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
pol := RecordTypePolicy(recordType)
|
||||
return delegation.Root(issuer, audience, cmd, pol, opts...)
|
||||
}
|
||||
|
||||
// NewDWNReadDelegation creates a delegation to read DWN records of a specific type.
|
||||
func NewDWNReadDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewDWNDelegation(issuer, audience, DWNRecordsRead, recordType, opts...)
|
||||
}
|
||||
|
||||
// NewDWNWriteDelegation creates a delegation to write DWN records of a specific type.
|
||||
func NewDWNWriteDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return NewDWNDelegation(issuer, audience, DWNRecordsWrite, recordType, opts...)
|
||||
}
|
||||
|
||||
// NewRootCapabilityDelegation creates a root delegation granting all capabilities.
|
||||
// Use with extreme caution - this grants full authority.
|
||||
func NewRootCapabilityDelegation(issuer, audience did.DID, opts ...delegation.Option) (*delegation.Token, error) {
|
||||
return delegation.Root(issuer, audience, Root, EmptyPolicy(), opts...)
|
||||
}
|
||||
259
internal/crypto/ucan/invocation.go
Normal file
259
internal/crypto/ucan/invocation.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.sonr.org/go/did-it"
|
||||
"code.sonr.org/go/did-it/crypto"
|
||||
"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.
|
||||
// An invocation exercises a delegated capability to perform an action.
|
||||
type InvocationBuilder struct {
|
||||
issuer did.DID
|
||||
subject did.DID
|
||||
cmd command.Command
|
||||
proofs []cid.Cid
|
||||
opts []invocation.Option
|
||||
err error
|
||||
}
|
||||
|
||||
// NewInvocationBuilder creates a builder for constructing UCAN invocations.
|
||||
func NewInvocationBuilder() *InvocationBuilder {
|
||||
return &InvocationBuilder{
|
||||
proofs: make([]cid.Cid, 0),
|
||||
opts: make([]invocation.Option, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Issuer sets the invocation issuer (the entity invoking the capability).
|
||||
func (b *InvocationBuilder) Issuer(iss did.DID) *InvocationBuilder {
|
||||
b.issuer = iss
|
||||
return b
|
||||
}
|
||||
|
||||
// IssuerString parses and sets the issuer from a DID string.
|
||||
func (b *InvocationBuilder) IssuerString(issStr string) *InvocationBuilder {
|
||||
iss, err := did.Parse(issStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid issuer DID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.issuer = iss
|
||||
return b
|
||||
}
|
||||
|
||||
// Subject sets the invocation subject (the resource being acted upon).
|
||||
func (b *InvocationBuilder) Subject(sub did.DID) *InvocationBuilder {
|
||||
b.subject = sub
|
||||
return b
|
||||
}
|
||||
|
||||
// SubjectString parses and sets the subject from a DID string.
|
||||
func (b *InvocationBuilder) SubjectString(subStr string) *InvocationBuilder {
|
||||
sub, err := did.Parse(subStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid subject DID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.subject = sub
|
||||
return b
|
||||
}
|
||||
|
||||
// Command sets the command being invoked.
|
||||
func (b *InvocationBuilder) Command(cmd command.Command) *InvocationBuilder {
|
||||
b.cmd = cmd
|
||||
return b
|
||||
}
|
||||
|
||||
// CommandString parses and sets the command from a string.
|
||||
func (b *InvocationBuilder) CommandString(cmdStr string) *InvocationBuilder {
|
||||
cmd, err := command.Parse(cmdStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid command: %w", err)
|
||||
return b
|
||||
}
|
||||
b.cmd = cmd
|
||||
return b
|
||||
}
|
||||
|
||||
// Proof adds a delegation CID to the proof chain.
|
||||
// Proofs should be ordered from leaf (matching invocation's issuer as audience)
|
||||
// to root delegation.
|
||||
func (b *InvocationBuilder) Proof(delegationCID cid.Cid) *InvocationBuilder {
|
||||
b.proofs = append(b.proofs, delegationCID)
|
||||
return b
|
||||
}
|
||||
|
||||
// ProofString parses and adds a delegation CID from a string.
|
||||
func (b *InvocationBuilder) ProofString(cidStr string) *InvocationBuilder {
|
||||
c, err := cid.Parse(cidStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid proof CID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.proofs = append(b.proofs, c)
|
||||
return b
|
||||
}
|
||||
|
||||
// Proofs sets all proofs at once, replacing any previously added.
|
||||
func (b *InvocationBuilder) Proofs(cids []cid.Cid) *InvocationBuilder {
|
||||
b.proofs = cids
|
||||
return b
|
||||
}
|
||||
|
||||
// Arg adds an argument to the invocation.
|
||||
func (b *InvocationBuilder) Arg(key string, value any) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithArgument(key, value))
|
||||
return b
|
||||
}
|
||||
|
||||
// Audience sets the invocation's audience (executor if different from subject).
|
||||
func (b *InvocationBuilder) Audience(aud did.DID) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithAudience(aud))
|
||||
return b
|
||||
}
|
||||
|
||||
// AudienceString parses and sets the audience from a DID string.
|
||||
func (b *InvocationBuilder) AudienceString(audStr string) *InvocationBuilder {
|
||||
aud, err := did.Parse(audStr)
|
||||
if err != nil {
|
||||
b.err = fmt.Errorf("invalid audience DID: %w", err)
|
||||
return b
|
||||
}
|
||||
b.opts = append(b.opts, invocation.WithAudience(aud))
|
||||
return b
|
||||
}
|
||||
|
||||
// ExpiresAt sets when the invocation expires.
|
||||
func (b *InvocationBuilder) ExpiresAt(exp time.Time) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithExpiration(exp))
|
||||
return b
|
||||
}
|
||||
|
||||
// ExpiresIn sets the invocation to expire after a duration from now.
|
||||
func (b *InvocationBuilder) ExpiresIn(d time.Duration) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithExpirationIn(d))
|
||||
return b
|
||||
}
|
||||
|
||||
// Meta adds metadata to the invocation.
|
||||
func (b *InvocationBuilder) Meta(key string, value any) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithMeta(key, value))
|
||||
return b
|
||||
}
|
||||
|
||||
// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated.
|
||||
func (b *InvocationBuilder) Nonce(nonce []byte) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithNonce(nonce))
|
||||
return b
|
||||
}
|
||||
|
||||
// EmptyNonce sets an empty nonce for idempotent operations.
|
||||
func (b *InvocationBuilder) EmptyNonce() *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithEmptyNonce())
|
||||
return b
|
||||
}
|
||||
|
||||
// Cause sets the receipt CID that enqueued this invocation.
|
||||
func (b *InvocationBuilder) Cause(receiptCID cid.Cid) *InvocationBuilder {
|
||||
b.opts = append(b.opts, invocation.WithCause(&receiptCID))
|
||||
return b
|
||||
}
|
||||
|
||||
// Build creates the invocation token.
|
||||
func (b *InvocationBuilder) Build() (*invocation.Token, error) {
|
||||
if b.err != nil {
|
||||
return nil, b.err
|
||||
}
|
||||
|
||||
if b.issuer == nil {
|
||||
return nil, fmt.Errorf("issuer is required")
|
||||
}
|
||||
if b.subject == nil {
|
||||
return nil, fmt.Errorf("subject is required")
|
||||
}
|
||||
if b.cmd == "" {
|
||||
return nil, fmt.Errorf("command is required")
|
||||
}
|
||||
|
||||
return invocation.New(b.issuer, b.cmd, b.subject, b.proofs, b.opts...)
|
||||
}
|
||||
|
||||
// BuildSealed creates and signs the invocation, returning DAG-CBOR bytes and CID.
|
||||
func (b *InvocationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) {
|
||||
tkn, err := b.Build()
|
||||
if err != nil {
|
||||
return nil, cid.Cid{}, err
|
||||
}
|
||||
return tkn.ToSealed(privKey)
|
||||
}
|
||||
|
||||
// --- Sonr-specific Invocation Builders ---
|
||||
|
||||
// VaultReadInvocation creates an invocation to read from a vault.
|
||||
func VaultReadInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := append([]invocation.Option{invocation.WithArgument("vault", vaultCID)}, opts...)
|
||||
return invocation.New(issuer, VaultRead, subject, proofs, allOpts...)
|
||||
}
|
||||
|
||||
// VaultWriteInvocation creates an invocation to write to a vault.
|
||||
func VaultWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, data any, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := append([]invocation.Option{
|
||||
invocation.WithArgument("vault", vaultCID),
|
||||
invocation.WithArgument("data", data),
|
||||
}, opts...)
|
||||
return invocation.New(issuer, VaultWrite, subject, proofs, allOpts...)
|
||||
}
|
||||
|
||||
// VaultSignInvocation creates an invocation to sign with a key in a vault.
|
||||
func VaultSignInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, keyID string, payload []byte, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := append([]invocation.Option{
|
||||
invocation.WithArgument("vault", vaultCID),
|
||||
invocation.WithArgument("key_id", keyID),
|
||||
invocation.WithArgument("payload", payload),
|
||||
}, opts...)
|
||||
return invocation.New(issuer, VaultSign, subject, proofs, allOpts...)
|
||||
}
|
||||
|
||||
// DIDUpdateInvocation creates an invocation to update a DID document.
|
||||
func DIDUpdateInvocation(issuer, subject did.DID, proofs []cid.Cid, targetDID string, updates map[string]any, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := append([]invocation.Option{
|
||||
invocation.WithArgument("did", targetDID),
|
||||
invocation.WithArgument("updates", updates),
|
||||
}, opts...)
|
||||
return invocation.New(issuer, DIDUpdate, subject, proofs, allOpts...)
|
||||
}
|
||||
|
||||
// DWNRecordsWriteInvocation creates an invocation to write a DWN record.
|
||||
func DWNRecordsWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, data any, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := append([]invocation.Option{
|
||||
invocation.WithArgument("record_type", recordType),
|
||||
invocation.WithArgument("data", data),
|
||||
}, opts...)
|
||||
return invocation.New(issuer, DWNRecordsWrite, subject, proofs, allOpts...)
|
||||
}
|
||||
|
||||
// DWNRecordsReadInvocation creates an invocation to read DWN records.
|
||||
func DWNRecordsReadInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, filter map[string]any, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := []invocation.Option{
|
||||
invocation.WithArgument("record_type", recordType),
|
||||
}
|
||||
if filter != nil {
|
||||
allOpts = append(allOpts, invocation.WithArgument("filter", filter))
|
||||
}
|
||||
allOpts = append(allOpts, opts...)
|
||||
return invocation.New(issuer, DWNRecordsRead, subject, proofs, allOpts...)
|
||||
}
|
||||
|
||||
// RevocationInvocation creates an invocation to revoke a delegation.
|
||||
func RevocationInvocation(issuer, subject did.DID, proofs []cid.Cid, delegationCID cid.Cid, opts ...invocation.Option) (*invocation.Token, error) {
|
||||
allOpts := append([]invocation.Option{
|
||||
invocation.WithArgument("ucan", delegationCID),
|
||||
}, opts...)
|
||||
return invocation.New(issuer, UCANRevoke, subject, proofs, allOpts...)
|
||||
}
|
||||
213
internal/crypto/ucan/policy.go
Normal file
213
internal/crypto/ucan/policy.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"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.
|
||||
// Policies are arrays of statements that form an implicit AND.
|
||||
type PolicyBuilder struct {
|
||||
constructors []policy.Constructor
|
||||
}
|
||||
|
||||
// NewPolicy creates a new PolicyBuilder.
|
||||
func NewPolicy() *PolicyBuilder {
|
||||
return &PolicyBuilder{
|
||||
constructors: make([]policy.Constructor, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Build constructs the final Policy from all added statements.
|
||||
func (b *PolicyBuilder) Build() (Policy, error) {
|
||||
return policy.Construct(b.constructors...)
|
||||
}
|
||||
|
||||
// MustBuild constructs the final Policy, panicking on error.
|
||||
func (b *PolicyBuilder) MustBuild() Policy {
|
||||
return policy.MustConstruct(b.constructors...)
|
||||
}
|
||||
|
||||
// Equal adds an equality constraint: selector == value
|
||||
func (b *PolicyBuilder) Equal(selector string, value any) *PolicyBuilder {
|
||||
node, err := toIPLDNode(value)
|
||||
if err != nil {
|
||||
// Store error for Build() to return
|
||||
b.constructors = append(b.constructors, func() (policy.Statement, error) {
|
||||
return nil, err
|
||||
})
|
||||
return b
|
||||
}
|
||||
b.constructors = append(b.constructors, policy.Equal(selector, node))
|
||||
return b
|
||||
}
|
||||
|
||||
// NotEqual adds an inequality constraint: selector != value
|
||||
func (b *PolicyBuilder) NotEqual(selector string, value any) *PolicyBuilder {
|
||||
node, err := toIPLDNode(value)
|
||||
if err != nil {
|
||||
b.constructors = append(b.constructors, func() (policy.Statement, error) {
|
||||
return nil, err
|
||||
})
|
||||
return b
|
||||
}
|
||||
b.constructors = append(b.constructors, policy.NotEqual(selector, node))
|
||||
return b
|
||||
}
|
||||
|
||||
// GreaterThan adds a comparison constraint: selector > value
|
||||
func (b *PolicyBuilder) GreaterThan(selector string, value int64) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.GreaterThan(selector, literal.Int(value)))
|
||||
return b
|
||||
}
|
||||
|
||||
// GreaterThanOrEqual adds a comparison constraint: selector >= value
|
||||
func (b *PolicyBuilder) GreaterThanOrEqual(selector string, value int64) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.GreaterThanOrEqual(selector, literal.Int(value)))
|
||||
return b
|
||||
}
|
||||
|
||||
// LessThan adds a comparison constraint: selector < value
|
||||
func (b *PolicyBuilder) LessThan(selector string, value int64) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.LessThan(selector, literal.Int(value)))
|
||||
return b
|
||||
}
|
||||
|
||||
// LessThanOrEqual adds a comparison constraint: selector <= value
|
||||
func (b *PolicyBuilder) LessThanOrEqual(selector string, value int64) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.LessThanOrEqual(selector, literal.Int(value)))
|
||||
return b
|
||||
}
|
||||
|
||||
// Like adds a glob pattern constraint: selector matches pattern
|
||||
// Use * for wildcard, \* for literal asterisk.
|
||||
func (b *PolicyBuilder) Like(selector, pattern string) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.Like(selector, pattern))
|
||||
return b
|
||||
}
|
||||
|
||||
// Not negates a statement
|
||||
func (b *PolicyBuilder) Not(stmt policy.Constructor) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.Not(stmt))
|
||||
return b
|
||||
}
|
||||
|
||||
// And adds a logical AND of multiple statements
|
||||
func (b *PolicyBuilder) And(stmts ...policy.Constructor) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.And(stmts...))
|
||||
return b
|
||||
}
|
||||
|
||||
// Or adds a logical OR of multiple statements
|
||||
func (b *PolicyBuilder) Or(stmts ...policy.Constructor) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.Or(stmts...))
|
||||
return b
|
||||
}
|
||||
|
||||
// All adds a universal quantifier: all elements at selector must satisfy statement
|
||||
func (b *PolicyBuilder) All(selector string, stmt policy.Constructor) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.All(selector, stmt))
|
||||
return b
|
||||
}
|
||||
|
||||
// Any adds an existential quantifier: at least one element at selector must satisfy statement
|
||||
func (b *PolicyBuilder) Any(selector string, stmt policy.Constructor) *PolicyBuilder {
|
||||
b.constructors = append(b.constructors, policy.Any(selector, stmt))
|
||||
return b
|
||||
}
|
||||
|
||||
// toIPLDNode converts a Go value to an IPLD node for policy evaluation.
|
||||
func toIPLDNode(value any) (ipld.Node, error) {
|
||||
// Handle IPLD nodes directly
|
||||
if node, ok := value.(ipld.Node); ok {
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Use literal package for conversion
|
||||
return literal.Any(value)
|
||||
}
|
||||
|
||||
// --- Sonr-specific Policy Helpers ---
|
||||
|
||||
// VaultPolicy creates a policy that restricts operations to a specific vault.
|
||||
// The vault is identified by its CID.
|
||||
func VaultPolicy(vaultCID string) Policy {
|
||||
return NewPolicy().Equal(".vault", vaultCID).MustBuild()
|
||||
}
|
||||
|
||||
// DIDPolicy creates a policy that restricts operations to DIDs matching a pattern.
|
||||
// Use glob patterns: "did:sonr:*" matches all Sonr DIDs.
|
||||
func DIDPolicy(didPattern string) Policy {
|
||||
return NewPolicy().Like(".did", didPattern).MustBuild()
|
||||
}
|
||||
|
||||
// ChainPolicy creates a policy that restricts operations to a specific chain.
|
||||
func ChainPolicy(chainID string) Policy {
|
||||
return NewPolicy().Equal(".chain_id", chainID).MustBuild()
|
||||
}
|
||||
|
||||
// AccountPolicy creates a policy that restricts operations to a specific account address.
|
||||
func AccountPolicy(address string) Policy {
|
||||
return NewPolicy().Equal(".address", address).MustBuild()
|
||||
}
|
||||
|
||||
// RecordTypePolicy creates a policy for DWN operations on specific record types.
|
||||
func RecordTypePolicy(recordType string) Policy {
|
||||
return NewPolicy().Equal(".record_type", recordType).MustBuild()
|
||||
}
|
||||
|
||||
// CombinePolicies merges multiple policies into one (implicit AND).
|
||||
func CombinePolicies(policies ...Policy) Policy {
|
||||
combined := make(Policy, 0)
|
||||
for _, p := range policies {
|
||||
combined = append(combined, p...)
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
// EmptyPolicy returns an empty policy (no constraints).
|
||||
func EmptyPolicy() Policy {
|
||||
return Policy{}
|
||||
}
|
||||
|
||||
// --- Policy Constructor Helpers ---
|
||||
|
||||
// These return policy.Constructor for use with And/Or/Not/All/Any
|
||||
|
||||
// EqualTo creates an equality constructor for nested policy building.
|
||||
func EqualTo(selector string, value any) policy.Constructor {
|
||||
return func() (policy.Statement, error) {
|
||||
node, err := toIPLDNode(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policy.Equal(selector, node)()
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqualTo creates an inequality constructor for nested policy building.
|
||||
func NotEqualTo(selector string, value any) policy.Constructor {
|
||||
return func() (policy.Statement, error) {
|
||||
node, err := toIPLDNode(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policy.NotEqual(selector, node)()
|
||||
}
|
||||
}
|
||||
|
||||
// Matches creates a glob pattern constructor for nested policy building.
|
||||
func Matches(selector, pattern string) policy.Constructor {
|
||||
return policy.Like(selector, pattern)
|
||||
}
|
||||
|
||||
// GreaterThanValue creates a comparison constructor.
|
||||
func GreaterThanValue(selector string, value int64) policy.Constructor {
|
||||
return policy.GreaterThan(selector, literal.Int(value))
|
||||
}
|
||||
|
||||
// LessThanValue creates a comparison constructor.
|
||||
func LessThanValue(selector string, value int64) policy.Constructor {
|
||||
return policy.LessThan(selector, literal.Int(value))
|
||||
}
|
||||
261
internal/crypto/ucan/types.go
Normal file
261
internal/crypto/ucan/types.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"code.sonr.org/go/ucan/pkg/policy"
|
||||
"github.com/ipfs/go-cid"
|
||||
)
|
||||
|
||||
// ValidationErrorCode represents UCAN validation error types.
|
||||
// These codes match the TypeScript ValidationErrorCode type in src/ucan.ts.
|
||||
type ValidationErrorCode string
|
||||
|
||||
const (
|
||||
ErrCodeExpired ValidationErrorCode = "EXPIRED"
|
||||
ErrCodeNotYetValid ValidationErrorCode = "NOT_YET_VALID"
|
||||
ErrCodeInvalidSignature ValidationErrorCode = "INVALID_SIGNATURE"
|
||||
ErrCodePrincipalMisaligned ValidationErrorCode = "PRINCIPAL_MISALIGNMENT"
|
||||
ErrCodePolicyViolation ValidationErrorCode = "POLICY_VIOLATION"
|
||||
ErrCodeRevoked ValidationErrorCode = "REVOKED"
|
||||
ErrCodeInvalidProofChain ValidationErrorCode = "INVALID_PROOF_CHAIN"
|
||||
ErrCodeUnknownCommand ValidationErrorCode = "UNKNOWN_COMMAND"
|
||||
ErrCodeMalformedToken ValidationErrorCode = "MALFORMED_TOKEN"
|
||||
)
|
||||
|
||||
// ValidationError represents a UCAN validation failure.
|
||||
type ValidationError struct {
|
||||
Code ValidationErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]any `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewValidationError creates a validation error with the given code and message.
|
||||
func NewValidationError(code ValidationErrorCode, message string) *ValidationError {
|
||||
return &ValidationError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NewValidationErrorWithDetails creates a validation error with additional details.
|
||||
func NewValidationErrorWithDetails(code ValidationErrorCode, message string, details map[string]any) *ValidationError {
|
||||
return &ValidationError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
// Capability represents the semantically-relevant claim of a delegation.
|
||||
// It combines subject, command, and policy into a single authorization unit.
|
||||
type Capability struct {
|
||||
// Subject DID (resource owner) - nil for powerline delegations
|
||||
Subject string `json:"sub"`
|
||||
// Command being delegated
|
||||
Command string `json:"cmd"`
|
||||
// Policy constraints on invocation arguments
|
||||
Policy policy.Policy `json:"pol"`
|
||||
}
|
||||
|
||||
// ValidationResult represents the outcome of UCAN validation.
|
||||
type ValidationResult struct {
|
||||
Valid bool `json:"valid"`
|
||||
Capability *Capability `json:"capability,omitempty"`
|
||||
Error *ValidationError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationSuccess creates a successful validation result.
|
||||
func ValidationSuccess(cap *Capability) *ValidationResult {
|
||||
return &ValidationResult{
|
||||
Valid: true,
|
||||
Capability: cap,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationFailure creates a failed validation result.
|
||||
func ValidationFailure(err *ValidationError) *ValidationResult {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// CryptoAlgorithm represents supported signature algorithms.
|
||||
type CryptoAlgorithm string
|
||||
|
||||
const (
|
||||
AlgorithmEd25519 CryptoAlgorithm = "Ed25519"
|
||||
AlgorithmP256 CryptoAlgorithm = "P-256"
|
||||
AlgorithmSecp256k1 CryptoAlgorithm = "secp256k1"
|
||||
)
|
||||
|
||||
// ExecutionResult represents the outcome of an invocation execution.
|
||||
type ExecutionResult[T any, E any] struct {
|
||||
Ok *T `json:"ok,omitempty"`
|
||||
Err *E `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
// IsSuccess returns true if the result is successful.
|
||||
func (r *ExecutionResult[T, E]) IsSuccess() bool {
|
||||
return r.Ok != nil
|
||||
}
|
||||
|
||||
// IsError returns true if the result is an error.
|
||||
func (r *ExecutionResult[T, E]) IsError() bool {
|
||||
return r.Err != nil
|
||||
}
|
||||
|
||||
// Success creates a successful execution result.
|
||||
func Success[T any, E any](value T) *ExecutionResult[T, E] {
|
||||
return &ExecutionResult[T, E]{Ok: &value}
|
||||
}
|
||||
|
||||
// Failure creates a failed execution result.
|
||||
func Failure[T any, E any](err E) *ExecutionResult[T, E] {
|
||||
return &ExecutionResult[T, E]{Err: &err}
|
||||
}
|
||||
|
||||
// ReceiptPayload represents the result of an invocation execution.
|
||||
// This matches the TypeScript ReceiptPayload in src/ucan.ts.
|
||||
type ReceiptPayload struct {
|
||||
// Executor DID
|
||||
Issuer string `json:"iss"`
|
||||
// CID of executed invocation
|
||||
Ran cid.Cid `json:"ran"`
|
||||
// Execution result
|
||||
Out *ExecutionResult[any, any] `json:"out"`
|
||||
// Effects - CIDs of Tasks to enqueue
|
||||
Effects []cid.Cid `json:"fx,omitempty"`
|
||||
// Optional metadata
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
// Issuance timestamp (Unix seconds)
|
||||
IssuedAt *int64 `json:"iat,omitempty"`
|
||||
}
|
||||
|
||||
// RevocationPayload represents a UCAN revocation request.
|
||||
// This matches the TypeScript RevocationPayload in src/ucan.ts.
|
||||
type RevocationPayload struct {
|
||||
// Revoker DID - must be issuer in delegation chain
|
||||
Issuer string `json:"iss"`
|
||||
// Subject of delegation being revoked
|
||||
Subject string `json:"sub"`
|
||||
// Revocation command (always "/ucan/revoke")
|
||||
Command string `json:"cmd"`
|
||||
// Revocation arguments
|
||||
Args RevocationArgs `json:"args"`
|
||||
// Proof chain
|
||||
Proof []cid.Cid `json:"prf"`
|
||||
// Nonce
|
||||
Nonce []byte `json:"nonce"`
|
||||
// Expiration (Unix seconds or null)
|
||||
Expiration *int64 `json:"exp"`
|
||||
}
|
||||
|
||||
// RevocationArgs contains the arguments for a revocation invocation.
|
||||
type RevocationArgs struct {
|
||||
// CID of delegation to revoke
|
||||
UCAN cid.Cid `json:"ucan"`
|
||||
}
|
||||
|
||||
// Task represents a subset of Invocation fields that uniquely determine work.
|
||||
// The Task ID is the CID of these fields.
|
||||
type Task struct {
|
||||
// Subject DID
|
||||
Subject string `json:"sub"`
|
||||
// Command to execute
|
||||
Command string `json:"cmd"`
|
||||
// Command arguments
|
||||
Args map[string]any `json:"args"`
|
||||
// Nonce for uniqueness
|
||||
Nonce []byte `json:"nonce"`
|
||||
}
|
||||
|
||||
// --- Sonr-specific Types ---
|
||||
|
||||
// VaultCapability represents authorization for vault operations.
|
||||
type VaultCapability struct {
|
||||
// VaultCID identifies the specific vault
|
||||
VaultCID string `json:"vault_cid"`
|
||||
// Operations allowed (read, write, sign, export, import, delete, admin)
|
||||
Operations []string `json:"operations"`
|
||||
}
|
||||
|
||||
// DIDCapability represents authorization for DID operations.
|
||||
type DIDCapability struct {
|
||||
// DIDPattern is a glob pattern matching allowed DIDs
|
||||
DIDPattern string `json:"did_pattern"`
|
||||
// Operations allowed (create, update, deactivate)
|
||||
Operations []string `json:"operations"`
|
||||
}
|
||||
|
||||
// DWNCapability represents authorization for DWN operations.
|
||||
type DWNCapability struct {
|
||||
// RecordType specifies the allowed record type
|
||||
RecordType string `json:"record_type"`
|
||||
// Operations allowed (read, write, delete)
|
||||
Operations []string `json:"operations"`
|
||||
}
|
||||
|
||||
// AccountCapability represents authorization for account operations.
|
||||
type AccountCapability struct {
|
||||
// ChainID specifies the blockchain
|
||||
ChainID string `json:"chain_id"`
|
||||
// Address specifies the account (or "*" for all)
|
||||
Address string `json:"address"`
|
||||
// Operations allowed
|
||||
Operations []string `json:"operations"`
|
||||
}
|
||||
|
||||
// SealedToken represents a signed UCAN token with its CID and raw bytes.
|
||||
type SealedToken struct {
|
||||
// CID is the content identifier of the sealed token
|
||||
CID cid.Cid `json:"cid"`
|
||||
// Data is the DAG-CBOR encoded envelope
|
||||
Data []byte `json:"data"`
|
||||
// Type indicates if this is a delegation or invocation
|
||||
Type string `json:"type"` // "delegation" or "invocation"
|
||||
}
|
||||
|
||||
// SealedDelegation is a type alias for a sealed delegation token.
|
||||
type SealedDelegation = SealedToken
|
||||
|
||||
// SealedInvocation is a type alias for a sealed invocation token.
|
||||
type SealedInvocation = SealedToken
|
||||
|
||||
// ProofChain represents an ordered list of delegation CIDs.
|
||||
// Ordered from leaf (matching invocation issuer) to root delegation.
|
||||
type ProofChain []cid.Cid
|
||||
|
||||
// NewProofChain creates a new proof chain from CIDs.
|
||||
func NewProofChain(cids ...cid.Cid) ProofChain {
|
||||
return ProofChain(cids)
|
||||
}
|
||||
|
||||
// Add appends a CID to the proof chain.
|
||||
func (p *ProofChain) Add(c cid.Cid) {
|
||||
*p = append(*p, c)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the proof chain is empty.
|
||||
func (p ProofChain) IsEmpty() bool {
|
||||
return len(p) == 0
|
||||
}
|
||||
|
||||
// Root returns the root delegation CID (last in chain).
|
||||
func (p ProofChain) Root() (cid.Cid, bool) {
|
||||
if len(p) == 0 {
|
||||
return cid.Cid{}, false
|
||||
}
|
||||
return p[len(p)-1], true
|
||||
}
|
||||
|
||||
// Leaf returns the leaf delegation CID (first in chain).
|
||||
func (p ProofChain) Leaf() (cid.Cid, bool) {
|
||||
if len(p) == 0 {
|
||||
return cid.Cid{}, false
|
||||
}
|
||||
return p[0], true
|
||||
}
|
||||
195
internal/crypto/ucan/ucan.go
Normal file
195
internal/crypto/ucan/ucan.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Package ucan provides UCAN v1.0.0-rc.1 compliant authorization
|
||||
// for the Sonr network using the official go-ucan library.
|
||||
//
|
||||
// This package wraps code.sonr.org/go/ucan to provide:
|
||||
// - Delegation creation and validation
|
||||
// - Invocation creation and validation
|
||||
// - Policy evaluation
|
||||
// - Sonr-specific capability types (vault, did, dwn)
|
||||
//
|
||||
// UCAN Envelope Format (DAG-CBOR):
|
||||
//
|
||||
// [
|
||||
// Signature, // Varsig-encoded signature
|
||||
// {
|
||||
// "h": VarsigHeader, // Algorithm metadata
|
||||
// "ucan/dlg@1.0.0-rc.1": DelegationPayload // or "ucan/inv@1.0.0-rc.1"
|
||||
// }
|
||||
// ]
|
||||
package ucan
|
||||
|
||||
import (
|
||||
"code.sonr.org/go/ucan/pkg/command"
|
||||
"code.sonr.org/go/ucan/pkg/policy"
|
||||
"code.sonr.org/go/ucan/token/delegation"
|
||||
"code.sonr.org/go/ucan/token/invocation"
|
||||
)
|
||||
|
||||
// Re-export key types from go-ucan for convenience.
|
||||
// Users should import this package instead of go-ucan directly
|
||||
// for Sonr-specific functionality.
|
||||
type (
|
||||
// Delegation is an immutable UCAN delegation token.
|
||||
Delegation = delegation.Token
|
||||
|
||||
// Invocation is an immutable UCAN invocation token.
|
||||
Invocation = invocation.Token
|
||||
|
||||
// Command is a validated UCAN command string (e.g., "/vault/read").
|
||||
Command = command.Command
|
||||
|
||||
// Policy is a list of policy statements that constrain invocation arguments.
|
||||
Policy = policy.Policy
|
||||
|
||||
// Statement is a single policy statement (equality, like, and, or, etc.).
|
||||
Statement = policy.Statement
|
||||
|
||||
// DelegationOption configures optional fields when creating a delegation.
|
||||
DelegationOption = delegation.Option
|
||||
|
||||
// InvocationOption configures optional fields when creating an invocation.
|
||||
InvocationOption = invocation.Option
|
||||
)
|
||||
|
||||
// Re-export constructors
|
||||
var (
|
||||
// NewDelegation creates a delegation: "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
||||
NewDelegation = delegation.New
|
||||
|
||||
// NewRootDelegation creates a root delegation where subject == issuer.
|
||||
NewRootDelegation = delegation.Root
|
||||
|
||||
// NewPowerlineDelegation creates a powerline delegation (subject = nil).
|
||||
// Powerline automatically delegates all future delegations regardless of subject.
|
||||
NewPowerlineDelegation = delegation.Powerline
|
||||
|
||||
// NewInvocation creates an invocation: "(issuer) executes (command) on (subject)".
|
||||
NewInvocation = invocation.New
|
||||
|
||||
// ParseCommand validates and parses a command string.
|
||||
ParseCommand = command.Parse
|
||||
|
||||
// MustParseCommand parses a command string, panicking on error.
|
||||
MustParseCommand = command.MustParse
|
||||
|
||||
// TopCommand returns "/" - the most powerful capability (grants everything).
|
||||
TopCommand = command.Top
|
||||
|
||||
// NewCommand creates a command from segments (e.g., NewCommand("vault", "read") -> "/vault/read").
|
||||
NewCommand = command.New
|
||||
)
|
||||
|
||||
// Re-export delegation options
|
||||
var (
|
||||
// WithExpiration sets the delegation's expiration time.
|
||||
WithExpiration = delegation.WithExpiration
|
||||
|
||||
// WithExpirationIn sets expiration to now + duration.
|
||||
WithExpirationIn = delegation.WithExpirationIn
|
||||
|
||||
// WithNotBefore sets when the delegation becomes valid.
|
||||
WithNotBefore = delegation.WithNotBefore
|
||||
|
||||
// WithNotBeforeIn sets not-before to now + duration.
|
||||
WithNotBeforeIn = delegation.WithNotBeforeIn
|
||||
|
||||
// WithDelegationMeta adds metadata to the delegation.
|
||||
WithDelegationMeta = delegation.WithMeta
|
||||
|
||||
// WithDelegationNonce sets a custom nonce (default: random 12 bytes).
|
||||
WithDelegationNonce = delegation.WithNonce
|
||||
)
|
||||
|
||||
// Re-export invocation options
|
||||
var (
|
||||
// WithArgument adds a single argument to the invocation.
|
||||
WithArgument = invocation.WithArgument
|
||||
|
||||
// WithAudience sets the invocation's audience (executor if different from subject).
|
||||
WithAudience = invocation.WithAudience
|
||||
|
||||
// WithInvocationMeta adds metadata to the invocation.
|
||||
WithInvocationMeta = invocation.WithMeta
|
||||
|
||||
// WithInvocationNonce sets a custom nonce.
|
||||
WithInvocationNonce = invocation.WithNonce
|
||||
|
||||
// WithEmptyNonce sets an empty nonce for idempotent operations.
|
||||
WithEmptyNonce = invocation.WithEmptyNonce
|
||||
|
||||
// WithInvocationExpiration sets the invocation's expiration time.
|
||||
WithInvocationExpiration = invocation.WithExpiration
|
||||
|
||||
// WithInvocationExpirationIn sets expiration to now + duration.
|
||||
WithInvocationExpirationIn = invocation.WithExpirationIn
|
||||
|
||||
// WithIssuedAt sets when the invocation was created.
|
||||
WithIssuedAt = invocation.WithIssuedAt
|
||||
|
||||
// WithCause sets the receipt CID that enqueued this task.
|
||||
WithCause = invocation.WithCause
|
||||
)
|
||||
|
||||
// Standard Sonr commands following UCAN v1.0.0-rc.1 command format.
|
||||
// Commands must be lowercase, start with '/', and have no trailing slash.
|
||||
const (
|
||||
// Vault commands
|
||||
CmdVaultRead = "/vault/read"
|
||||
CmdVaultWrite = "/vault/write"
|
||||
CmdVaultSign = "/vault/sign"
|
||||
CmdVaultExport = "/vault/export"
|
||||
CmdVaultImport = "/vault/import"
|
||||
CmdVaultDelete = "/vault/delete"
|
||||
CmdVaultAdmin = "/vault/admin"
|
||||
CmdVault = "/vault" // Superuser - grants all vault commands
|
||||
|
||||
// DID commands
|
||||
CmdDIDCreate = "/did/create"
|
||||
CmdDIDUpdate = "/did/update"
|
||||
CmdDIDDeactivate = "/did/deactivate"
|
||||
CmdDID = "/did" // Superuser - grants all DID commands
|
||||
|
||||
// DWN commands
|
||||
CmdDWNRecordsWrite = "/dwn/records/write"
|
||||
CmdDWNRecordsRead = "/dwn/records/read"
|
||||
CmdDWNRecordsDelete = "/dwn/records/delete"
|
||||
CmdDWN = "/dwn" // Superuser - grants all DWN commands
|
||||
|
||||
// UCAN meta commands
|
||||
CmdUCANRevoke = "/ucan/revoke"
|
||||
|
||||
// Root command - grants everything
|
||||
CmdRoot = "/"
|
||||
)
|
||||
|
||||
// Pre-parsed Sonr commands for convenience
|
||||
var (
|
||||
VaultRead = command.MustParse(CmdVaultRead)
|
||||
VaultWrite = command.MustParse(CmdVaultWrite)
|
||||
VaultSign = command.MustParse(CmdVaultSign)
|
||||
VaultExport = command.MustParse(CmdVaultExport)
|
||||
VaultImport = command.MustParse(CmdVaultImport)
|
||||
VaultDelete = command.MustParse(CmdVaultDelete)
|
||||
VaultAdmin = command.MustParse(CmdVaultAdmin)
|
||||
Vault = command.MustParse(CmdVault)
|
||||
|
||||
DIDCreate = command.MustParse(CmdDIDCreate)
|
||||
DIDUpdate = command.MustParse(CmdDIDUpdate)
|
||||
DIDDeactivate = command.MustParse(CmdDIDDeactivate)
|
||||
DID = command.MustParse(CmdDID)
|
||||
|
||||
DWNRecordsWrite = command.MustParse(CmdDWNRecordsWrite)
|
||||
DWNRecordsRead = command.MustParse(CmdDWNRecordsRead)
|
||||
DWNRecordsDelete = command.MustParse(CmdDWNRecordsDelete)
|
||||
DWN = command.MustParse(CmdDWN)
|
||||
|
||||
UCANRevoke = command.MustParse(CmdUCANRevoke)
|
||||
Root = command.Top()
|
||||
)
|
||||
|
||||
// CommandSubsumes checks if parent command subsumes child command.
|
||||
// A command subsumes another if the child is a path extension of parent.
|
||||
// Example: "/vault" subsumes "/vault/read" and "/vault/write"
|
||||
func CommandSubsumes(parent, child Command) bool {
|
||||
return parent.Covers(child)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (am *ActionManager) ListAccounts(ctx context.Context) ([]AccountResult, err
|
||||
AddressIndex: row.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: row.IsDefault == 1,
|
||||
PublicKey: row.PublicKey,
|
||||
PublicKey: row.PublicKeyHex,
|
||||
Curve: row.Curve,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
@@ -245,12 +245,19 @@ func (am *ActionManager) ListSessions(ctx context.Context) ([]SessionResult, err
|
||||
authenticator = *sess.Authenticator
|
||||
}
|
||||
|
||||
var deviceInfo json.RawMessage
|
||||
if sess.DeviceInfo != nil {
|
||||
deviceInfo = json.RawMessage(*sess.DeviceInfo)
|
||||
} else {
|
||||
deviceInfo = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
results[i] = SessionResult{
|
||||
ID: sess.ID,
|
||||
SessionID: sess.SessionID,
|
||||
DeviceName: sess.DeviceName,
|
||||
Authenticator: authenticator,
|
||||
DeviceInfo: sess.DeviceInfo,
|
||||
DeviceInfo: deviceInfo,
|
||||
IsCurrent: sess.IsCurrent == 1,
|
||||
LastActivity: sess.LastActivity,
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
@@ -386,8 +393,8 @@ func (am *ActionManager) ListGrants(ctx context.Context) ([]GrantResult, error)
|
||||
ServiceName: g.ServiceName,
|
||||
ServiceOrigin: g.ServiceOrigin,
|
||||
ServiceLogo: serviceLogo,
|
||||
Scopes: g.Scopes,
|
||||
Accounts: g.Accounts,
|
||||
Scopes: json.RawMessage(g.Scopes),
|
||||
Accounts: json.RawMessage(g.Accounts),
|
||||
Status: g.Status,
|
||||
GrantedAt: g.GrantedAt,
|
||||
LastUsed: lastUsed,
|
||||
@@ -481,7 +488,7 @@ func (am *ActionManager) ResolveDID(ctx context.Context, did string) (*DIDDocume
|
||||
AddressIndex: row.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: row.IsDefault == 1,
|
||||
PublicKey: row.PublicKey,
|
||||
PublicKey: row.PublicKeyHex,
|
||||
Curve: row.Curve,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
|
||||
178
internal/keybase/actions_account.go
Normal file
178
internal/keybase/actions_account.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type NewAccountInput struct {
|
||||
EnclaveID int64 `json:"enclave_id"`
|
||||
Address string `json:"address"`
|
||||
ChainID string `json:"chain_id"`
|
||||
CoinType int64 `json:"coin_type"`
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
AddressIndex int64 `json:"address_index"`
|
||||
Label string `json:"label,omitempty"`
|
||||
IsDefault int64 `json:"is_default,omitempty"`
|
||||
}
|
||||
|
||||
func (am *ActionManager) CreateAccount(ctx context.Context, params NewAccountInput) (*AccountResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
var label *string
|
||||
if params.Label != "" {
|
||||
label = ¶ms.Label
|
||||
}
|
||||
|
||||
acc, err := am.kb.queries.CreateAccount(ctx, CreateAccountParams{
|
||||
DidID: am.kb.didID,
|
||||
EnclaveID: params.EnclaveID,
|
||||
Address: params.Address,
|
||||
ChainID: params.ChainID,
|
||||
CoinType: params.CoinType,
|
||||
AccountIndex: params.AccountIndex,
|
||||
AddressIndex: params.AddressIndex,
|
||||
Label: label,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create account: %w", err)
|
||||
}
|
||||
|
||||
labelStr := ""
|
||||
if acc.Label != nil {
|
||||
labelStr = *acc.Label
|
||||
}
|
||||
|
||||
return &AccountResult{
|
||||
ID: acc.ID,
|
||||
Address: acc.Address,
|
||||
ChainID: acc.ChainID,
|
||||
CoinType: acc.CoinType,
|
||||
AccountIndex: acc.AccountIndex,
|
||||
AddressIndex: acc.AddressIndex,
|
||||
Label: labelStr,
|
||||
IsDefault: acc.IsDefault == 1,
|
||||
CreatedAt: acc.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) ListAccountsByChain(ctx context.Context, chainID string) ([]AccountResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []AccountResult{}, nil
|
||||
}
|
||||
|
||||
accounts, err := am.kb.queries.ListAccountsByChain(ctx, ListAccountsByChainParams{
|
||||
DidID: am.kb.didID,
|
||||
ChainID: chainID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list accounts by chain: %w", err)
|
||||
}
|
||||
|
||||
results := make([]AccountResult, len(accounts))
|
||||
for i, acc := range accounts {
|
||||
label := ""
|
||||
if acc.Label != nil {
|
||||
label = *acc.Label
|
||||
}
|
||||
results[i] = AccountResult{
|
||||
ID: acc.ID,
|
||||
Address: acc.Address,
|
||||
ChainID: acc.ChainID,
|
||||
CoinType: acc.CoinType,
|
||||
AccountIndex: acc.AccountIndex,
|
||||
AddressIndex: acc.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: acc.IsDefault == 1,
|
||||
CreatedAt: acc.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetDefaultAccount(ctx context.Context, chainID string) (*AccountResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
acc, err := am.kb.queries.GetDefaultAccount(ctx, GetDefaultAccountParams{
|
||||
DidID: am.kb.didID,
|
||||
ChainID: chainID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get default account: %w", err)
|
||||
}
|
||||
|
||||
label := ""
|
||||
if acc.Label != nil {
|
||||
label = *acc.Label
|
||||
}
|
||||
|
||||
return &AccountResult{
|
||||
ID: acc.ID,
|
||||
Address: acc.Address,
|
||||
ChainID: acc.ChainID,
|
||||
CoinType: acc.CoinType,
|
||||
AccountIndex: acc.AccountIndex,
|
||||
AddressIndex: acc.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: acc.IsDefault == 1,
|
||||
CreatedAt: acc.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) SetDefaultAccount(ctx context.Context, accountID int64, chainID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
return am.kb.queries.SetDefaultAccount(ctx, SetDefaultAccountParams{
|
||||
ID: accountID,
|
||||
DidID: am.kb.didID,
|
||||
ChainID: chainID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) UpdateAccountLabel(ctx context.Context, accountID int64, label string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
var labelPtr *string
|
||||
if label != "" {
|
||||
labelPtr = &label
|
||||
}
|
||||
|
||||
return am.kb.queries.UpdateAccountLabel(ctx, UpdateAccountLabelParams{
|
||||
Label: labelPtr,
|
||||
ID: accountID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) DeleteAccount(ctx context.Context, accountID int64) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
return am.kb.queries.DeleteAccount(ctx, DeleteAccountParams{
|
||||
ID: accountID,
|
||||
DidID: am.kb.didID,
|
||||
})
|
||||
}
|
||||
155
internal/keybase/actions_credential.go
Normal file
155
internal/keybase/actions_credential.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type NewCredentialInput struct {
|
||||
CredentialID string `json:"credential_id"`
|
||||
PublicKey string `json:"public_key"`
|
||||
PublicKeyAlg int64 `json:"public_key_alg"`
|
||||
AAGUID string `json:"aaguid,omitempty"`
|
||||
Transports []string `json:"transports"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
Authenticator string `json:"authenticator,omitempty"`
|
||||
IsDiscoverable bool `json:"is_discoverable"`
|
||||
BackedUp bool `json:"backed_up"`
|
||||
}
|
||||
|
||||
func (am *ActionManager) CreateCredential(ctx context.Context, params NewCredentialInput) (*CredentialResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
var aaguid, authenticator *string
|
||||
if params.AAGUID != "" {
|
||||
aaguid = ¶ms.AAGUID
|
||||
}
|
||||
if params.Authenticator != "" {
|
||||
authenticator = ¶ms.Authenticator
|
||||
}
|
||||
|
||||
transports, err := json.Marshal(params.Transports)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal transports: %w", err)
|
||||
}
|
||||
|
||||
var isDiscoverable, backedUp int64
|
||||
if params.IsDiscoverable {
|
||||
isDiscoverable = 1
|
||||
}
|
||||
if params.BackedUp {
|
||||
backedUp = 1
|
||||
}
|
||||
|
||||
cred, err := am.kb.queries.CreateCredential(ctx, CreateCredentialParams{
|
||||
DidID: am.kb.didID,
|
||||
CredentialID: params.CredentialID,
|
||||
PublicKey: params.PublicKey,
|
||||
PublicKeyAlg: params.PublicKeyAlg,
|
||||
Aaguid: aaguid,
|
||||
Transports: transports,
|
||||
DeviceName: params.DeviceName,
|
||||
DeviceType: params.DeviceType,
|
||||
Authenticator: authenticator,
|
||||
IsDiscoverable: isDiscoverable,
|
||||
BackedUp: backedUp,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create credential: %w", err)
|
||||
}
|
||||
|
||||
return credentialToResult(&cred), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) UpdateCredentialCounter(ctx context.Context, credentialID string, signCount int64) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get credential: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.UpdateCredentialCounter(ctx, UpdateCredentialCounterParams{
|
||||
SignCount: signCount,
|
||||
ID: cred.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) RenameCredential(ctx context.Context, credentialID string, newName string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get credential: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.RenameCredential(ctx, RenameCredentialParams{
|
||||
DeviceName: newName,
|
||||
ID: cred.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) DeleteCredential(ctx context.Context, credentialID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get credential: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.DeleteCredential(ctx, DeleteCredentialParams{
|
||||
ID: cred.ID,
|
||||
DidID: am.kb.didID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) CountCredentialsByDID(ctx context.Context) (int64, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return am.kb.queries.CountCredentialsByDID(ctx, am.kb.didID)
|
||||
}
|
||||
|
||||
func credentialToResult(cred *Credential) *CredentialResult {
|
||||
var transports []string
|
||||
if err := json.Unmarshal(cred.Transports, &transports); err != nil {
|
||||
transports = []string{}
|
||||
}
|
||||
|
||||
authenticator := ""
|
||||
if cred.Authenticator != nil {
|
||||
authenticator = *cred.Authenticator
|
||||
}
|
||||
|
||||
return &CredentialResult{
|
||||
ID: cred.ID,
|
||||
CredentialID: cred.CredentialID,
|
||||
DeviceName: cred.DeviceName,
|
||||
DeviceType: cred.DeviceType,
|
||||
Authenticator: authenticator,
|
||||
Transports: transports,
|
||||
SignCount: cred.SignCount,
|
||||
IsDiscoverable: cred.IsDiscoverable == 1,
|
||||
BackedUp: cred.BackedUp == 1,
|
||||
CreatedAt: cred.CreatedAt,
|
||||
LastUsed: cred.LastUsed,
|
||||
}
|
||||
}
|
||||
318
internal/keybase/actions_delegation.go
Normal file
318
internal/keybase/actions_delegation.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// DELEGATION ACTIONS (UCAN v1.0.0-rc.1)
|
||||
// =============================================================================
|
||||
|
||||
// DelegationResult represents a delegation in API responses.
|
||||
type DelegationResult struct {
|
||||
ID int64 `json:"id"`
|
||||
CID string `json:"cid"`
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Command string `json:"cmd"`
|
||||
Policy string `json:"pol,omitempty"`
|
||||
NotBefore string `json:"nbf,omitempty"`
|
||||
Expiration string `json:"exp,omitempty"`
|
||||
IsRoot bool `json:"is_root"`
|
||||
IsPowerline bool `json:"is_powerline"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// StoreDelegationParams contains parameters for storing a delegation.
|
||||
type StoreDelegationParams struct {
|
||||
CID string `json:"cid"`
|
||||
Envelope []byte `json:"envelope"`
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Command string `json:"cmd"`
|
||||
Policy string `json:"pol,omitempty"`
|
||||
NotBefore string `json:"nbf,omitempty"`
|
||||
Expiration string `json:"exp,omitempty"`
|
||||
IsRoot bool `json:"is_root"`
|
||||
IsPowerline bool `json:"is_powerline"`
|
||||
}
|
||||
|
||||
// StoreDelegation stores a new UCAN delegation envelope.
|
||||
func (am *ActionManager) StoreDelegation(ctx context.Context, params StoreDelegationParams) (*DelegationResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
var sub, pol, nbf, exp *string
|
||||
if params.Subject != "" {
|
||||
sub = ¶ms.Subject
|
||||
}
|
||||
if params.Policy != "" {
|
||||
pol = ¶ms.Policy
|
||||
}
|
||||
if params.NotBefore != "" {
|
||||
nbf = ¶ms.NotBefore
|
||||
}
|
||||
if params.Expiration != "" {
|
||||
exp = ¶ms.Expiration
|
||||
}
|
||||
|
||||
isRoot := int64(0)
|
||||
if params.IsRoot {
|
||||
isRoot = 1
|
||||
}
|
||||
isPowerline := int64(0)
|
||||
if params.IsPowerline {
|
||||
isPowerline = 1
|
||||
}
|
||||
|
||||
d, err := am.kb.queries.CreateDelegation(ctx, CreateDelegationParams{
|
||||
DidID: am.kb.didID,
|
||||
Cid: params.CID,
|
||||
Envelope: params.Envelope,
|
||||
Iss: params.Issuer,
|
||||
Aud: params.Audience,
|
||||
Sub: sub,
|
||||
Cmd: params.Command,
|
||||
Pol: pol,
|
||||
Nbf: nbf,
|
||||
Exp: exp,
|
||||
IsRoot: isRoot,
|
||||
IsPowerline: isPowerline,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create delegation: %w", err)
|
||||
}
|
||||
|
||||
return delegationToResult(d), nil
|
||||
}
|
||||
|
||||
// GetDelegationByCID retrieves a delegation by its CID.
|
||||
func (am *ActionManager) GetDelegationByCID(ctx context.Context, cid string) (*DelegationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
d, err := am.kb.queries.GetDelegationByCID(ctx, cid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get delegation: %w", err)
|
||||
}
|
||||
|
||||
return delegationToResult(d), nil
|
||||
}
|
||||
|
||||
// GetDelegationEnvelope retrieves the raw CBOR envelope for a delegation.
|
||||
func (am *ActionManager) GetDelegationEnvelope(ctx context.Context, cid string) ([]byte, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
envelope, err := am.kb.queries.GetDelegationEnvelopeByCID(ctx, cid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get delegation envelope: %w", err)
|
||||
}
|
||||
|
||||
return envelope, nil
|
||||
}
|
||||
|
||||
// ListDelegations returns all active delegations for the current DID.
|
||||
func (am *ActionManager) ListDelegations(ctx context.Context) ([]DelegationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []DelegationResult{}, nil
|
||||
}
|
||||
|
||||
delegations, err := am.kb.queries.ListDelegationsByDID(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list delegations: %w", err)
|
||||
}
|
||||
|
||||
results := make([]DelegationResult, len(delegations))
|
||||
for i, d := range delegations {
|
||||
results[i] = *delegationToResult(d)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListDelegationsByIssuer returns delegations issued by a specific DID.
|
||||
func (am *ActionManager) ListDelegationsByIssuer(ctx context.Context, issuer string) ([]DelegationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
delegations, err := am.kb.queries.ListDelegationsByIssuer(ctx, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list delegations by issuer: %w", err)
|
||||
}
|
||||
|
||||
results := make([]DelegationResult, len(delegations))
|
||||
for i, d := range delegations {
|
||||
results[i] = *delegationToResult(d)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListDelegationsByAudience returns delegations granted to a specific DID.
|
||||
func (am *ActionManager) ListDelegationsByAudience(ctx context.Context, audience string) ([]DelegationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
delegations, err := am.kb.queries.ListDelegationsByAudience(ctx, audience)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list delegations by audience: %w", err)
|
||||
}
|
||||
|
||||
results := make([]DelegationResult, len(delegations))
|
||||
for i, d := range delegations {
|
||||
results[i] = *delegationToResult(d)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListDelegationsForCommand returns delegations that grant a specific command.
|
||||
func (am *ActionManager) ListDelegationsForCommand(ctx context.Context, cmd string) ([]DelegationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []DelegationResult{}, nil
|
||||
}
|
||||
|
||||
delegations, err := am.kb.queries.ListDelegationsForCommand(ctx, ListDelegationsForCommandParams{
|
||||
DidID: am.kb.didID,
|
||||
Cmd: cmd,
|
||||
Cmd_2: cmd,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list delegations for command: %w", err)
|
||||
}
|
||||
|
||||
results := make([]DelegationResult, len(delegations))
|
||||
for i, d := range delegations {
|
||||
results[i] = *delegationToResult(d)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// IsDelegationRevoked checks if a delegation has been revoked.
|
||||
func (am *ActionManager) IsDelegationRevoked(ctx context.Context, cid string) (bool, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
revoked, err := am.kb.queries.IsDelegationRevoked(ctx, cid)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("check revocation: %w", err)
|
||||
}
|
||||
|
||||
return revoked == 1, nil
|
||||
}
|
||||
|
||||
// RevokeDelegationParams contains parameters for revoking a delegation.
|
||||
type RevokeDelegationParams struct {
|
||||
DelegationCID string `json:"delegation_cid"`
|
||||
RevokedBy string `json:"revoked_by"`
|
||||
InvocationCID string `json:"invocation_cid,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// RevokeDelegation revokes a delegation by creating a revocation record.
|
||||
func (am *ActionManager) RevokeDelegation(ctx context.Context, params RevokeDelegationParams) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
var invocationCID, reason *string
|
||||
if params.InvocationCID != "" {
|
||||
invocationCID = ¶ms.InvocationCID
|
||||
}
|
||||
if params.Reason != "" {
|
||||
reason = ¶ms.Reason
|
||||
}
|
||||
|
||||
err := am.kb.queries.CreateRevocation(ctx, CreateRevocationParams{
|
||||
DelegationCid: params.DelegationCID,
|
||||
RevokedBy: params.RevokedBy,
|
||||
InvocationCid: invocationCID,
|
||||
Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create revocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDelegation deletes a delegation from the database.
|
||||
func (am *ActionManager) DeleteDelegation(ctx context.Context, cid string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
err := am.kb.queries.DeleteDelegation(ctx, DeleteDelegationParams{
|
||||
Cid: cid,
|
||||
DidID: am.kb.didID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete delegation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanExpiredDelegations removes delegations expired more than 30 days ago.
|
||||
func (am *ActionManager) CleanExpiredDelegations(ctx context.Context) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if err := am.kb.queries.CleanExpiredDelegations(ctx); err != nil {
|
||||
return fmt.Errorf("clean expired delegations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// delegationToResult converts a UcanDelegation to DelegationResult.
|
||||
func delegationToResult(d UcanDelegation) *DelegationResult {
|
||||
subject := ""
|
||||
if d.Sub != nil {
|
||||
subject = *d.Sub
|
||||
}
|
||||
policy := ""
|
||||
if d.Pol != nil {
|
||||
policy = *d.Pol
|
||||
}
|
||||
notBefore := ""
|
||||
if d.Nbf != nil {
|
||||
notBefore = *d.Nbf
|
||||
}
|
||||
expiration := ""
|
||||
if d.Exp != nil {
|
||||
expiration = *d.Exp
|
||||
}
|
||||
|
||||
return &DelegationResult{
|
||||
ID: d.ID,
|
||||
CID: d.Cid,
|
||||
Issuer: d.Iss,
|
||||
Audience: d.Aud,
|
||||
Subject: subject,
|
||||
Command: d.Cmd,
|
||||
Policy: policy,
|
||||
NotBefore: notBefore,
|
||||
Expiration: expiration,
|
||||
IsRoot: d.IsRoot == 1,
|
||||
IsPowerline: d.IsPowerline == 1,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
}
|
||||
169
internal/keybase/actions_enclave.go
Normal file
169
internal/keybase/actions_enclave.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"enclave/internal/crypto/mpc"
|
||||
)
|
||||
|
||||
type EnclaveResult struct {
|
||||
ID int64 `json:"id"`
|
||||
EnclaveID string `json:"enclave_id"`
|
||||
PublicKeyHex string `json:"public_key_hex"`
|
||||
Curve string `json:"curve"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
RotatedAt string `json:"rotated_at,omitempty"`
|
||||
}
|
||||
|
||||
type NewEnclaveInput struct {
|
||||
EnclaveID string `json:"enclave_id"`
|
||||
PublicKeyHex string `json:"public_key_hex"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
ValShare []byte `json:"val_share"`
|
||||
UserShare []byte `json:"user_share"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
Curve string `json:"curve"`
|
||||
}
|
||||
|
||||
func (am *ActionManager) CreateEnclave(ctx context.Context, params NewEnclaveInput) (*EnclaveResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
enc, err := am.kb.queries.CreateEnclave(ctx, CreateEnclaveParams{
|
||||
DidID: am.kb.didID,
|
||||
EnclaveID: params.EnclaveID,
|
||||
PublicKeyHex: params.PublicKeyHex,
|
||||
PublicKey: params.PublicKey,
|
||||
ValShare: params.ValShare,
|
||||
UserShare: params.UserShare,
|
||||
Nonce: params.Nonce,
|
||||
Curve: params.Curve,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create enclave: %w", err)
|
||||
}
|
||||
|
||||
return enclaveToResult(&enc), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) ListEnclaves(ctx context.Context) ([]EnclaveResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []EnclaveResult{}, nil
|
||||
}
|
||||
|
||||
enclaves, err := am.kb.queries.ListEnclavesByDID(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list enclaves: %w", err)
|
||||
}
|
||||
|
||||
results := make([]EnclaveResult, len(enclaves))
|
||||
for i, enc := range enclaves {
|
||||
results[i] = *enclaveToResult(&enc)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetEnclaveByID(ctx context.Context, enclaveID string) (*EnclaveResult, 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)
|
||||
}
|
||||
|
||||
return enclaveToResult(&enc), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) RotateEnclave(ctx context.Context, enclaveID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get enclave: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.RotateEnclave(ctx, enc.ID)
|
||||
}
|
||||
|
||||
func (am *ActionManager) ArchiveEnclave(ctx context.Context, enclaveID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get enclave: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.ArchiveEnclave(ctx, enc.ID)
|
||||
}
|
||||
|
||||
func (am *ActionManager) DeleteEnclave(ctx context.Context, enclaveID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
enc, err := am.kb.queries.GetEnclaveByID(ctx, enclaveID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get enclave: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.DeleteEnclave(ctx, DeleteEnclaveParams{
|
||||
ID: enc.ID,
|
||||
DidID: am.kb.didID,
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
rotatedAt = *enc.RotatedAt
|
||||
}
|
||||
|
||||
return &EnclaveResult{
|
||||
ID: enc.ID,
|
||||
EnclaveID: enc.EnclaveID,
|
||||
PublicKeyHex: enc.PublicKeyHex,
|
||||
Curve: enc.Curve,
|
||||
Status: enc.Status,
|
||||
CreatedAt: enc.CreatedAt,
|
||||
RotatedAt: rotatedAt,
|
||||
}
|
||||
}
|
||||
181
internal/keybase/actions_grant.go
Normal file
181
internal/keybase/actions_grant.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type NewGrantInput struct {
|
||||
ServiceID int64 `json:"service_id"`
|
||||
DelegationCID string `json:"delegation_cid,omitempty"`
|
||||
Scopes json.RawMessage `json:"scopes"`
|
||||
Accounts json.RawMessage `json:"accounts"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (am *ActionManager) CreateGrant(ctx context.Context, params NewGrantInput) (*GrantResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
var delegationCID *string
|
||||
if params.DelegationCID != "" {
|
||||
delegationCID = ¶ms.DelegationCID
|
||||
}
|
||||
|
||||
var expiresAt *string
|
||||
if params.ExpiresAt != "" {
|
||||
expiresAt = ¶ms.ExpiresAt
|
||||
}
|
||||
|
||||
scopes := params.Scopes
|
||||
if scopes == nil {
|
||||
scopes = json.RawMessage(`[]`)
|
||||
}
|
||||
accounts := params.Accounts
|
||||
if accounts == nil {
|
||||
accounts = json.RawMessage(`[]`)
|
||||
}
|
||||
|
||||
g, err := am.kb.queries.CreateGrant(ctx, CreateGrantParams{
|
||||
DidID: am.kb.didID,
|
||||
ServiceID: params.ServiceID,
|
||||
DelegationCid: delegationCID,
|
||||
Scopes: scopes,
|
||||
Accounts: accounts,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create grant: %w", err)
|
||||
}
|
||||
|
||||
svc, err := am.kb.queries.GetServiceByID(ctx, g.ServiceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get service: %w", err)
|
||||
}
|
||||
|
||||
serviceLogo := ""
|
||||
if svc.LogoUrl != nil {
|
||||
serviceLogo = *svc.LogoUrl
|
||||
}
|
||||
|
||||
lastUsed := ""
|
||||
if g.LastUsed != nil {
|
||||
lastUsed = *g.LastUsed
|
||||
}
|
||||
|
||||
expires := ""
|
||||
if g.ExpiresAt != nil {
|
||||
expires = *g.ExpiresAt
|
||||
}
|
||||
|
||||
return &GrantResult{
|
||||
ID: g.ID,
|
||||
ServiceName: svc.Name,
|
||||
ServiceOrigin: svc.Origin,
|
||||
ServiceLogo: serviceLogo,
|
||||
Scopes: g.Scopes,
|
||||
Accounts: g.Accounts,
|
||||
Status: g.Status,
|
||||
GrantedAt: g.GrantedAt,
|
||||
LastUsed: lastUsed,
|
||||
ExpiresAt: expires,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetGrantByService(ctx context.Context, serviceID int64) (*GrantResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
g, err := am.kb.queries.GetGrantByService(ctx, GetGrantByServiceParams{
|
||||
DidID: am.kb.didID,
|
||||
ServiceID: serviceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get grant by service: %w", err)
|
||||
}
|
||||
|
||||
svc, err := am.kb.queries.GetServiceByID(ctx, g.ServiceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get service: %w", err)
|
||||
}
|
||||
|
||||
serviceLogo := ""
|
||||
if svc.LogoUrl != nil {
|
||||
serviceLogo = *svc.LogoUrl
|
||||
}
|
||||
|
||||
lastUsed := ""
|
||||
if g.LastUsed != nil {
|
||||
lastUsed = *g.LastUsed
|
||||
}
|
||||
|
||||
expires := ""
|
||||
if g.ExpiresAt != nil {
|
||||
expires = *g.ExpiresAt
|
||||
}
|
||||
|
||||
return &GrantResult{
|
||||
ID: g.ID,
|
||||
ServiceName: svc.Name,
|
||||
ServiceOrigin: svc.Origin,
|
||||
ServiceLogo: serviceLogo,
|
||||
Scopes: g.Scopes,
|
||||
Accounts: g.Accounts,
|
||||
Status: g.Status,
|
||||
GrantedAt: g.GrantedAt,
|
||||
LastUsed: lastUsed,
|
||||
ExpiresAt: expires,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) UpdateGrantScopes(ctx context.Context, grantID int64, scopes, accounts json.RawMessage) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
return am.kb.queries.UpdateGrantScopes(ctx, UpdateGrantScopesParams{
|
||||
Scopes: scopes,
|
||||
Accounts: accounts,
|
||||
ID: grantID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) UpdateGrantLastUsed(ctx context.Context, grantID int64) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
return am.kb.queries.UpdateGrantLastUsed(ctx, grantID)
|
||||
}
|
||||
|
||||
func (am *ActionManager) SuspendGrant(ctx context.Context, grantID int64) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
return am.kb.queries.SuspendGrant(ctx, grantID)
|
||||
}
|
||||
|
||||
func (am *ActionManager) ReactivateGrant(ctx context.Context, grantID int64) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
return am.kb.queries.ReactivateGrant(ctx, grantID)
|
||||
}
|
||||
|
||||
func (am *ActionManager) CountActiveGrants(ctx context.Context) (int64, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return am.kb.queries.CountActiveGrants(ctx, am.kb.didID)
|
||||
}
|
||||
257
internal/keybase/actions_invocation.go
Normal file
257
internal/keybase/actions_invocation.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// INVOCATION ACTIONS (UCAN v1.0.0-rc.1)
|
||||
// =============================================================================
|
||||
|
||||
// InvocationResult represents an invocation in API responses.
|
||||
type InvocationResult struct {
|
||||
ID int64 `json:"id"`
|
||||
CID string `json:"cid"`
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Audience string `json:"aud,omitempty"`
|
||||
Command string `json:"cmd"`
|
||||
Proofs string `json:"prf"`
|
||||
Expiration string `json:"exp,omitempty"`
|
||||
IssuedAt string `json:"iat,omitempty"`
|
||||
ExecutedAt string `json:"executed_at,omitempty"`
|
||||
ResultCID string `json:"result_cid,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// StoreInvocationParams contains parameters for storing an invocation.
|
||||
type StoreInvocationParams struct {
|
||||
CID string `json:"cid"`
|
||||
Envelope []byte `json:"envelope"`
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Audience string `json:"aud,omitempty"`
|
||||
Command string `json:"cmd"`
|
||||
Proofs string `json:"prf"`
|
||||
Expiration string `json:"exp,omitempty"`
|
||||
IssuedAt string `json:"iat,omitempty"`
|
||||
}
|
||||
|
||||
// StoreInvocation stores a new UCAN invocation envelope.
|
||||
func (am *ActionManager) StoreInvocation(ctx context.Context, params StoreInvocationParams) (*InvocationResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
var aud, exp, iat *string
|
||||
if params.Audience != "" {
|
||||
aud = ¶ms.Audience
|
||||
}
|
||||
if params.Expiration != "" {
|
||||
exp = ¶ms.Expiration
|
||||
}
|
||||
if params.IssuedAt != "" {
|
||||
iat = ¶ms.IssuedAt
|
||||
}
|
||||
|
||||
inv, err := am.kb.queries.CreateInvocation(ctx, CreateInvocationParams{
|
||||
DidID: am.kb.didID,
|
||||
Cid: params.CID,
|
||||
Envelope: params.Envelope,
|
||||
Iss: params.Issuer,
|
||||
Sub: params.Subject,
|
||||
Aud: aud,
|
||||
Cmd: params.Command,
|
||||
Prf: params.Proofs,
|
||||
Exp: exp,
|
||||
Iat: iat,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create invocation: %w", err)
|
||||
}
|
||||
|
||||
return invocationToResult(inv), nil
|
||||
}
|
||||
|
||||
// GetInvocationByCID retrieves an invocation by its CID.
|
||||
func (am *ActionManager) GetInvocationByCID(ctx context.Context, cid string) (*InvocationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
inv, err := am.kb.queries.GetInvocationByCID(ctx, cid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get invocation: %w", err)
|
||||
}
|
||||
|
||||
return invocationToResult(inv), nil
|
||||
}
|
||||
|
||||
// GetInvocationEnvelope retrieves the raw CBOR envelope for an invocation.
|
||||
func (am *ActionManager) GetInvocationEnvelope(ctx context.Context, cid string) ([]byte, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
envelope, err := am.kb.queries.GetInvocationEnvelopeByCID(ctx, cid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get invocation envelope: %w", err)
|
||||
}
|
||||
|
||||
return envelope, nil
|
||||
}
|
||||
|
||||
// ListInvocations returns recent invocations for the current DID.
|
||||
func (am *ActionManager) ListInvocations(ctx context.Context, limit int64) ([]InvocationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []InvocationResult{}, nil
|
||||
}
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
invocations, err := am.kb.queries.ListInvocationsByDID(ctx, ListInvocationsByDIDParams{
|
||||
DidID: am.kb.didID,
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list invocations: %w", err)
|
||||
}
|
||||
|
||||
results := make([]InvocationResult, len(invocations))
|
||||
for i, inv := range invocations {
|
||||
results[i] = *invocationToResult(inv)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListInvocationsByCommand returns invocations for a specific command.
|
||||
func (am *ActionManager) ListInvocationsByCommand(ctx context.Context, cmd string, limit int64) ([]InvocationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []InvocationResult{}, nil
|
||||
}
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
invocations, err := am.kb.queries.ListInvocationsForCommand(ctx, ListInvocationsForCommandParams{
|
||||
DidID: am.kb.didID,
|
||||
Cmd: cmd,
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list invocations for command: %w", err)
|
||||
}
|
||||
|
||||
results := make([]InvocationResult, len(invocations))
|
||||
for i, inv := range invocations {
|
||||
results[i] = *invocationToResult(inv)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListPendingInvocations returns invocations that haven't been executed yet.
|
||||
func (am *ActionManager) ListPendingInvocations(ctx context.Context) ([]InvocationResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []InvocationResult{}, nil
|
||||
}
|
||||
|
||||
invocations, err := am.kb.queries.ListPendingInvocations(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list pending invocations: %w", err)
|
||||
}
|
||||
|
||||
results := make([]InvocationResult, len(invocations))
|
||||
for i, inv := range invocations {
|
||||
results[i] = *invocationToResult(inv)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// MarkInvocationExecuted marks an invocation as executed with an optional result CID.
|
||||
func (am *ActionManager) MarkInvocationExecuted(ctx context.Context, cid string, resultCID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
var result *string
|
||||
if resultCID != "" {
|
||||
result = &resultCID
|
||||
}
|
||||
|
||||
err := am.kb.queries.MarkInvocationExecuted(ctx, MarkInvocationExecutedParams{
|
||||
ResultCid: result,
|
||||
Cid: cid,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark invocation executed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanOldInvocations removes invocations older than 90 days.
|
||||
func (am *ActionManager) CleanOldInvocations(ctx context.Context) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if err := am.kb.queries.CleanOldInvocations(ctx); err != nil {
|
||||
return fmt.Errorf("clean old invocations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// invocationToResult converts a UcanInvocation to InvocationResult.
|
||||
func invocationToResult(inv UcanInvocation) *InvocationResult {
|
||||
audience := ""
|
||||
if inv.Aud != nil {
|
||||
audience = *inv.Aud
|
||||
}
|
||||
expiration := ""
|
||||
if inv.Exp != nil {
|
||||
expiration = *inv.Exp
|
||||
}
|
||||
issuedAt := ""
|
||||
if inv.Iat != nil {
|
||||
issuedAt = *inv.Iat
|
||||
}
|
||||
executedAt := ""
|
||||
if inv.ExecutedAt != nil {
|
||||
executedAt = *inv.ExecutedAt
|
||||
}
|
||||
resultCID := ""
|
||||
if inv.ResultCid != nil {
|
||||
resultCID = *inv.ResultCid
|
||||
}
|
||||
|
||||
return &InvocationResult{
|
||||
ID: inv.ID,
|
||||
CID: inv.Cid,
|
||||
Issuer: inv.Iss,
|
||||
Subject: inv.Sub,
|
||||
Audience: audience,
|
||||
Command: inv.Cmd,
|
||||
Proofs: inv.Prf,
|
||||
Expiration: expiration,
|
||||
IssuedAt: issuedAt,
|
||||
ExecutedAt: executedAt,
|
||||
ResultCID: resultCID,
|
||||
CreatedAt: inv.CreatedAt,
|
||||
}
|
||||
}
|
||||
172
internal/keybase/actions_service.go
Normal file
172
internal/keybase/actions_service.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ServiceResult struct {
|
||||
ID int64 `json:"id"`
|
||||
Origin string `json:"origin"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
LogoURL string `json:"logo_url,omitempty"`
|
||||
DID string `json:"did,omitempty"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type NewServiceInput struct {
|
||||
Origin string `json:"origin"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
LogoURL string `json:"logo_url,omitempty"`
|
||||
DID string `json:"did,omitempty"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateServiceInput struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
LogoURL string `json:"logo_url,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (am *ActionManager) CreateService(ctx context.Context, params NewServiceInput) (*ServiceResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
var description, logoURL, did *string
|
||||
if params.Description != "" {
|
||||
description = ¶ms.Description
|
||||
}
|
||||
if params.LogoURL != "" {
|
||||
logoURL = ¶ms.LogoURL
|
||||
}
|
||||
if params.DID != "" {
|
||||
did = ¶ms.DID
|
||||
}
|
||||
|
||||
metadata := params.Metadata
|
||||
if metadata == nil {
|
||||
metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
var isVerified int64
|
||||
if params.IsVerified {
|
||||
isVerified = 1
|
||||
}
|
||||
|
||||
svc, err := am.kb.queries.CreateService(ctx, CreateServiceParams{
|
||||
Origin: params.Origin,
|
||||
Name: params.Name,
|
||||
Description: description,
|
||||
LogoUrl: logoURL,
|
||||
Did: did,
|
||||
IsVerified: isVerified,
|
||||
Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create service: %w", err)
|
||||
}
|
||||
|
||||
return serviceToResult(&svc), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetServiceByOrigin(ctx context.Context, origin string) (*ServiceResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
svc, err := am.kb.queries.GetServiceByOrigin(ctx, origin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get service by origin: %w", err)
|
||||
}
|
||||
|
||||
return serviceToResult(&svc), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetServiceByID(ctx context.Context, serviceID int64) (*ServiceResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
svc, err := am.kb.queries.GetServiceByID(ctx, serviceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get service by ID: %w", err)
|
||||
}
|
||||
|
||||
return serviceToResult(&svc), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) UpdateService(ctx context.Context, params UpdateServiceInput) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
var description, logoURL *string
|
||||
if params.Description != "" {
|
||||
description = ¶ms.Description
|
||||
}
|
||||
if params.LogoURL != "" {
|
||||
logoURL = ¶ms.LogoURL
|
||||
}
|
||||
|
||||
metadata := params.Metadata
|
||||
if metadata == nil {
|
||||
metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
return am.kb.queries.UpdateService(ctx, UpdateServiceParams{
|
||||
Name: params.Name,
|
||||
Description: description,
|
||||
LogoUrl: logoURL,
|
||||
Metadata: metadata,
|
||||
ID: params.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) ListVerifiedServices(ctx context.Context) ([]ServiceResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
services, err := am.kb.queries.ListVerifiedServices(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list verified services: %w", err)
|
||||
}
|
||||
|
||||
results := make([]ServiceResult, len(services))
|
||||
for i, svc := range services {
|
||||
results[i] = *serviceToResult(&svc)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func serviceToResult(svc *Service) *ServiceResult {
|
||||
description := ""
|
||||
if svc.Description != nil {
|
||||
description = *svc.Description
|
||||
}
|
||||
logoURL := ""
|
||||
if svc.LogoUrl != nil {
|
||||
logoURL = *svc.LogoUrl
|
||||
}
|
||||
did := ""
|
||||
if svc.Did != nil {
|
||||
did = *svc.Did
|
||||
}
|
||||
|
||||
return &ServiceResult{
|
||||
ID: svc.ID,
|
||||
Origin: svc.Origin,
|
||||
Name: svc.Name,
|
||||
Description: description,
|
||||
LogoURL: logoURL,
|
||||
DID: did,
|
||||
IsVerified: svc.IsVerified == 1,
|
||||
Metadata: svc.Metadata,
|
||||
CreatedAt: svc.CreatedAt,
|
||||
}
|
||||
}
|
||||
84
internal/keybase/actions_session.go
Normal file
84
internal/keybase/actions_session.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (am *ActionManager) GetSessionByID(ctx context.Context, sessionID string) (*SessionResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get session: %w", err)
|
||||
}
|
||||
|
||||
return sessionToResult(&sess), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetCurrentSession(ctx context.Context) (*SessionResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
sess, err := am.kb.queries.GetCurrentSession(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get current session: %w", err)
|
||||
}
|
||||
|
||||
return sessionToResult(&sess), nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) UpdateSessionActivity(ctx context.Context, sessionID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get session: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.UpdateSessionActivity(ctx, sess.ID)
|
||||
}
|
||||
|
||||
func (am *ActionManager) SetCurrentSession(ctx context.Context, sessionID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get session: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.SetCurrentSession(ctx, SetCurrentSessionParams{
|
||||
ID: sess.ID,
|
||||
DidID: am.kb.didID,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *ActionManager) DeleteExpiredSessions(ctx context.Context) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
return am.kb.queries.DeleteExpiredSessions(ctx)
|
||||
}
|
||||
|
||||
func sessionToResult(sess *Session) *SessionResult {
|
||||
return &SessionResult{
|
||||
ID: sess.ID,
|
||||
SessionID: sess.SessionID,
|
||||
DeviceInfo: sess.DeviceInfo,
|
||||
IsCurrent: sess.IsCurrent == 1,
|
||||
LastActivity: sess.LastActivity,
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
CreatedAt: sess.CreatedAt,
|
||||
}
|
||||
}
|
||||
114
internal/keybase/actions_verification.go
Normal file
114
internal/keybase/actions_verification.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type NewVerificationMethodInput struct {
|
||||
MethodID string `json:"method_id"`
|
||||
MethodType string `json:"method_type"`
|
||||
Controller string `json:"controller"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
|
||||
func (am *ActionManager) CreateVerificationMethod(ctx context.Context, params NewVerificationMethodInput) (*VerificationMethodResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
vm, err := am.kb.queries.CreateVerificationMethod(ctx, CreateVerificationMethodParams{
|
||||
DidID: am.kb.didID,
|
||||
MethodID: params.MethodID,
|
||||
MethodType: params.MethodType,
|
||||
Controller: params.Controller,
|
||||
PublicKey: params.PublicKey,
|
||||
Purpose: params.Purpose,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create verification method: %w", err)
|
||||
}
|
||||
|
||||
return &VerificationMethodResult{
|
||||
ID: vm.MethodID,
|
||||
Type: vm.MethodType,
|
||||
Controller: vm.Controller,
|
||||
PublicKey: vm.PublicKey,
|
||||
Purpose: vm.Purpose,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) ListVerificationMethodsFull(ctx context.Context) ([]VerificationMethodResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []VerificationMethodResult{}, nil
|
||||
}
|
||||
|
||||
vms, err := am.kb.queries.ListVerificationMethods(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list verification methods: %w", err)
|
||||
}
|
||||
|
||||
results := make([]VerificationMethodResult, len(vms))
|
||||
for i, vm := range vms {
|
||||
results[i] = VerificationMethodResult{
|
||||
ID: vm.MethodID,
|
||||
Type: vm.MethodType,
|
||||
Controller: vm.Controller,
|
||||
PublicKey: vm.PublicKey,
|
||||
Purpose: vm.Purpose,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) GetVerificationMethod(ctx context.Context, methodID string) (*VerificationMethodResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
vm, err := am.kb.queries.GetVerificationMethod(ctx, GetVerificationMethodParams{
|
||||
DidID: am.kb.didID,
|
||||
MethodID: methodID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get verification method: %w", err)
|
||||
}
|
||||
|
||||
return &VerificationMethodResult{
|
||||
ID: vm.MethodID,
|
||||
Type: vm.MethodType,
|
||||
Controller: vm.Controller,
|
||||
PublicKey: vm.PublicKey,
|
||||
Purpose: vm.Purpose,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *ActionManager) DeleteVerificationMethod(ctx context.Context, methodID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
vm, err := am.kb.queries.GetVerificationMethod(ctx, GetVerificationMethodParams{
|
||||
DidID: am.kb.didID,
|
||||
MethodID: methodID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get verification method: %w", err)
|
||||
}
|
||||
|
||||
return am.kb.queries.DeleteVerificationMethod(ctx, vm.ID)
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
// Package keybase contains the SQLite database for cryptographic keys.
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"enclave/internal/migrations"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
// Keybase encapsulates the encrypted key storage database.
|
||||
type Keybase struct {
|
||||
db *sql.DB
|
||||
queries *Queries
|
||||
did string
|
||||
didID int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
instance *Keybase
|
||||
initMu sync.Mutex
|
||||
)
|
||||
|
||||
// Open creates or returns the singleton Keybase instance with an in-memory database.
|
||||
func Open() (*Keybase, error) {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
|
||||
if instance != nil {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
conn, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("keybase: open database: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Exec(migrations.SchemaSQL); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("keybase: init schema: %w", err)
|
||||
}
|
||||
|
||||
instance = &Keybase{
|
||||
db: conn,
|
||||
queries: New(conn),
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// Get returns the existing Keybase instance or nil if not initialized.
|
||||
func Get() *Keybase {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
return instance
|
||||
}
|
||||
|
||||
// MustGet returns the existing Keybase instance or panics if not initialized.
|
||||
func MustGet() *Keybase {
|
||||
kb := Get()
|
||||
if kb == nil {
|
||||
panic("keybase: not initialized")
|
||||
}
|
||||
return kb
|
||||
}
|
||||
|
||||
// Close closes the database connection and clears the singleton.
|
||||
func Close() error {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
|
||||
if instance == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := instance.db.Close()
|
||||
instance = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset clears the singleton instance (useful for testing).
|
||||
func Reset() {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
|
||||
if instance != nil {
|
||||
instance.db.Close()
|
||||
instance = nil
|
||||
}
|
||||
}
|
||||
|
||||
// DB returns the underlying sql.DB connection.
|
||||
func (k *Keybase) DB() *sql.DB {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.db
|
||||
}
|
||||
|
||||
// Queries returns the SQLC-generated query interface.
|
||||
func (k *Keybase) Queries() *Queries {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.queries
|
||||
}
|
||||
|
||||
// DID returns the current DID identifier.
|
||||
func (k *Keybase) DID() string {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.did
|
||||
}
|
||||
|
||||
// DIDID returns the database ID of the current DID.
|
||||
func (k *Keybase) DIDID() int64 {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.didID
|
||||
}
|
||||
|
||||
// IsInitialized returns true if a DID has been set.
|
||||
func (k *Keybase) IsInitialized() bool {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.did != ""
|
||||
}
|
||||
|
||||
// SetDID sets the current DID context.
|
||||
func (k *Keybase) SetDID(did string, didID int64) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.did = did
|
||||
k.didID = didID
|
||||
}
|
||||
|
||||
// Initialize creates a new DID document from a WebAuthn credential.
|
||||
func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (string, error) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16])
|
||||
docJSON, _ := json.Marshal(map[string]any{
|
||||
"@context": []string{"https://www.w3.org/ns/did/v1"},
|
||||
"id": did,
|
||||
})
|
||||
|
||||
doc, err := k.queries.CreateDID(ctx, CreateDIDParams{
|
||||
Did: did,
|
||||
Controller: did,
|
||||
Document: docJSON,
|
||||
Sequence: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("keybase: create DID: %w", err)
|
||||
}
|
||||
|
||||
k.did = did
|
||||
k.didID = doc.ID
|
||||
|
||||
return did, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return "", fmt.Errorf("keybase: invalid database format")
|
||||
}
|
||||
|
||||
docs, err := k.queries.ListAllDIDs(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("keybase: list DIDs: %w", err)
|
||||
}
|
||||
|
||||
if len(docs) == 0 {
|
||||
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.
|
||||
func (k *Keybase) Serialize() ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
if k.db == nil {
|
||||
return nil, fmt.Errorf("keybase: database not initialized")
|
||||
}
|
||||
|
||||
return k.exportDump()
|
||||
}
|
||||
|
||||
// exportDump creates a SQL dump of the database.
|
||||
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 {
|
||||
rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
values := make([]any, len(cols))
|
||||
valuePtrs := make([]any, len(cols))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&dump, "-- Row from %s\n", table)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
return []byte(dump.String()), nil
|
||||
}
|
||||
|
||||
// WithTx executes a function within a database transaction.
|
||||
func (k *Keybase) WithTx(ctx context.Context, fn func(*Queries) error) error {
|
||||
tx, err := k.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("keybase: begin tx: %w", err)
|
||||
}
|
||||
|
||||
if err := fn(k.queries.WithTx(tx)); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
|
||||
411
internal/keybase/exec.go
Normal file
411
internal/keybase/exec.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HandlerFunc func(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error)
|
||||
|
||||
type resourceHandlers map[string]HandlerFunc
|
||||
|
||||
var handlers = map[string]resourceHandlers{
|
||||
"accounts": {
|
||||
"list": handleAccountList,
|
||||
"get": handleAccountGet,
|
||||
"sign": handleAccountSign,
|
||||
},
|
||||
"credentials": {
|
||||
"list": handleCredentialList,
|
||||
"get": handleCredentialGet,
|
||||
},
|
||||
"sessions": {
|
||||
"list": handleSessionList,
|
||||
"revoke": handleSessionRevoke,
|
||||
},
|
||||
"grants": {
|
||||
"list": handleGrantList,
|
||||
"revoke": handleGrantRevoke,
|
||||
},
|
||||
"enclaves": {
|
||||
"list": handleEnclaveList,
|
||||
"get": handleEnclaveGet,
|
||||
"sign": handleEnclaveSign,
|
||||
"rotate": handleEnclaveRotate,
|
||||
"archive": handleEnclaveArchive,
|
||||
"delete": handleEnclaveDelete,
|
||||
},
|
||||
"delegations": {
|
||||
"list": handleDelegationList,
|
||||
"list_received": handleDelegationListReceived,
|
||||
"list_command": handleDelegationListCommand,
|
||||
"get": handleDelegationGet,
|
||||
"revoke": handleDelegationRevoke,
|
||||
"verify": handleDelegationVerify,
|
||||
"cleanup": handleDelegationCleanup,
|
||||
},
|
||||
"ucans": {
|
||||
"list": handleDelegationList,
|
||||
"get": handleDelegationGet,
|
||||
"revoke": handleDelegationRevoke,
|
||||
"verify": handleDelegationVerify,
|
||||
"cleanup": handleDelegationCleanup,
|
||||
},
|
||||
"verification_methods": {
|
||||
"list": handleVerificationMethodList,
|
||||
"get": handleVerificationMethodGet,
|
||||
"delete": handleVerificationMethodDelete,
|
||||
},
|
||||
"services": {
|
||||
"list": handleServiceList,
|
||||
"get": handleServiceGet,
|
||||
"get_by_id": handleServiceGetByID,
|
||||
},
|
||||
}
|
||||
|
||||
func Exec(ctx context.Context, resource, action, subject string) (json.RawMessage, error) {
|
||||
am, err := NewActionManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("action manager: %w", err)
|
||||
}
|
||||
|
||||
resHandlers, ok := handlers[resource]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown resource: %s", resource)
|
||||
}
|
||||
|
||||
handler, ok := resHandlers[action]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown action for %s: %s", resource, action)
|
||||
}
|
||||
|
||||
return handler(ctx, am, subject)
|
||||
}
|
||||
|
||||
func handleAccountList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
accounts, err := am.ListAccounts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(accounts)
|
||||
}
|
||||
|
||||
func handleAccountGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (address) required for get action")
|
||||
}
|
||||
account, err := am.GetAccountByAddress(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(credentials)
|
||||
}
|
||||
|
||||
func handleCredentialGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (credential_id) required for get action")
|
||||
}
|
||||
credential, err := am.GetCredentialByID(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(credential)
|
||||
}
|
||||
|
||||
func handleSessionList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
sessions, err := am.ListSessions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(sessions)
|
||||
}
|
||||
|
||||
func handleSessionRevoke(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (session_id) required for revoke action")
|
||||
}
|
||||
if err := am.RevokeSession(ctx, subject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"revoked": true})
|
||||
}
|
||||
|
||||
func handleGrantList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
grants, err := am.ListGrants(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(grants)
|
||||
}
|
||||
|
||||
func handleGrantRevoke(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (grant_id) required for revoke action")
|
||||
}
|
||||
var grantID int64
|
||||
if _, err := fmt.Sscanf(subject, "%d", &grantID); err != nil {
|
||||
return nil, fmt.Errorf("invalid grant_id: %w", err)
|
||||
}
|
||||
if err := am.RevokeGrant(ctx, grantID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"revoked": true})
|
||||
}
|
||||
|
||||
func handleEnclaveList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
enclaves, err := am.ListEnclaves(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(enclaves)
|
||||
}
|
||||
|
||||
func handleEnclaveGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (enclave_id) required for get action")
|
||||
}
|
||||
enc, err := am.GetEnclaveByID(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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")
|
||||
}
|
||||
if err := am.RotateEnclave(ctx, subject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"rotated": true})
|
||||
}
|
||||
|
||||
func handleEnclaveArchive(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (enclave_id) required for archive action")
|
||||
}
|
||||
if err := am.ArchiveEnclave(ctx, subject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"archived": true})
|
||||
}
|
||||
|
||||
func handleEnclaveDelete(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (enclave_id) required for delete action")
|
||||
}
|
||||
if err := am.DeleteEnclave(ctx, subject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
func handleDelegationList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
delegations, err := am.ListDelegations(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(delegations)
|
||||
}
|
||||
|
||||
func handleDelegationListReceived(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (audience DID) required for list_received action")
|
||||
}
|
||||
delegations, err := am.ListDelegationsByAudience(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(delegations)
|
||||
}
|
||||
|
||||
func handleDelegationListCommand(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (command) required for list_command action")
|
||||
}
|
||||
delegations, err := am.ListDelegationsForCommand(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(delegations)
|
||||
}
|
||||
|
||||
func handleDelegationGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (cid) required for get action")
|
||||
}
|
||||
delegation, err := am.GetDelegationByCID(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(delegation)
|
||||
}
|
||||
|
||||
func handleDelegationRevoke(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (cid) required for revoke action")
|
||||
}
|
||||
kb := Get()
|
||||
revokedBy := ""
|
||||
if kb != nil {
|
||||
revokedBy = kb.DID()
|
||||
}
|
||||
if err := am.RevokeDelegation(ctx, RevokeDelegationParams{
|
||||
DelegationCID: subject,
|
||||
RevokedBy: revokedBy,
|
||||
Reason: "user revoked",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"revoked": true})
|
||||
}
|
||||
|
||||
func handleDelegationVerify(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (cid) required for verify action")
|
||||
}
|
||||
revoked, err := am.IsDelegationRevoked(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked})
|
||||
}
|
||||
|
||||
func handleDelegationCleanup(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
if err := am.CleanExpiredDelegations(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"cleaned": true})
|
||||
}
|
||||
|
||||
func handleVerificationMethodList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
vms, err := am.ListVerificationMethodsFull(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(vms)
|
||||
}
|
||||
|
||||
func handleVerificationMethodGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (method_id) required for get action")
|
||||
}
|
||||
vm, err := am.GetVerificationMethod(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(vm)
|
||||
}
|
||||
|
||||
func handleVerificationMethodDelete(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (method_id) required for delete action")
|
||||
}
|
||||
if err := am.DeleteVerificationMethod(ctx, subject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
func handleServiceList(ctx context.Context, am *ActionManager, _ string) (json.RawMessage, error) {
|
||||
services, err := am.ListVerifiedServices(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(services)
|
||||
}
|
||||
|
||||
func handleServiceGet(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (origin) required for get action")
|
||||
}
|
||||
svc, err := am.GetServiceByOrigin(ctx, subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(svc)
|
||||
}
|
||||
|
||||
func handleServiceGetByID(ctx context.Context, am *ActionManager, subject string) (json.RawMessage, error) {
|
||||
if subject == "" {
|
||||
return nil, errors.New("subject (service_id) required for get_by_id action")
|
||||
}
|
||||
var serviceID int64
|
||||
if _, err := fmt.Sscanf(subject, "%d", &serviceID); err != nil {
|
||||
return nil, fmt.Errorf("invalid service_id: %w", err)
|
||||
}
|
||||
svc, err := am.GetServiceByID(ctx, serviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(svc)
|
||||
}
|
||||
311
internal/keybase/functions.go
Normal file
311
internal/keybase/functions.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"enclave/internal/crypto/bip44"
|
||||
"enclave/internal/crypto/mpc"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
func RegisterMPCFunctions(conn *sqlite3.Conn) error {
|
||||
if err := registerGenerateFunction(conn); err != nil {
|
||||
return fmt.Errorf("register mpc_generate: %w", err)
|
||||
}
|
||||
if err := registerSignFunction(conn); err != nil {
|
||||
return fmt.Errorf("register mpc_sign: %w", err)
|
||||
}
|
||||
if err := registerVerifyFunction(conn); err != nil {
|
||||
return fmt.Errorf("register mpc_verify: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func registerGenerateFunction(conn *sqlite3.Conn) error {
|
||||
return conn.CreateFunction("mpc_generate", 0, 0, func(ctx sqlite3.Context, args ...sqlite3.Value) {
|
||||
enc, err := mpc.NewSimpleEnclave()
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_generate: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
idBytes := make([]byte, 8)
|
||||
rand.Read(idBytes)
|
||||
enclaveID := fmt.Sprintf("enc_%x", idBytes)
|
||||
|
||||
kb := Get()
|
||||
if kb == nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_generate: keybase not initialized"))
|
||||
return
|
||||
}
|
||||
|
||||
am, err := NewActionManager()
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_generate: action manager: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = am.CreateEnclave(context.Background(), NewEnclaveInput{
|
||||
EnclaveID: enclaveID,
|
||||
PublicKeyHex: enc.PubKeyHex(),
|
||||
PublicKey: enc.PubKeyBytes(),
|
||||
ValShare: enc.GetShare1(),
|
||||
UserShare: enc.GetShare2(),
|
||||
Nonce: enc.GetNonce(),
|
||||
Curve: string(enc.GetCurve()),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_generate: create enclave: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ResultText(enclaveID)
|
||||
})
|
||||
}
|
||||
|
||||
func registerSignFunction(conn *sqlite3.Conn) error {
|
||||
return conn.CreateFunction("mpc_sign", 2, sqlite3.DETERMINISTIC, func(ctx sqlite3.Context, args ...sqlite3.Value) {
|
||||
if len(args) != 2 {
|
||||
ctx.ResultError(fmt.Errorf("mpc_sign requires 2 arguments: enclave_id, data"))
|
||||
return
|
||||
}
|
||||
|
||||
enclaveID := args[0].Text()
|
||||
data := args[1].RawBlob()
|
||||
|
||||
if enclaveID == "" {
|
||||
ctx.ResultError(fmt.Errorf("mpc_sign: enclave_id cannot be empty"))
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
ctx.ResultError(fmt.Errorf("mpc_sign: data cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
enclave, err := loadSimpleEnclaveFromDB(enclaveID)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_sign: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := enclave.Sign(data)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_sign: signing failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ResultBlob(signature)
|
||||
})
|
||||
}
|
||||
|
||||
func registerVerifyFunction(conn *sqlite3.Conn) error {
|
||||
return conn.CreateFunction("mpc_verify", 3, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, func(ctx sqlite3.Context, args ...sqlite3.Value) {
|
||||
if len(args) != 3 {
|
||||
ctx.ResultError(fmt.Errorf("mpc_verify requires 3 arguments: public_key_hex, data, signature"))
|
||||
return
|
||||
}
|
||||
|
||||
pubKeyHex := args[0].Text()
|
||||
data := args[1].RawBlob()
|
||||
signature := args[2].RawBlob()
|
||||
|
||||
if pubKeyHex == "" {
|
||||
ctx.ResultError(fmt.Errorf("mpc_verify: public_key_hex cannot be empty"))
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
ctx.ResultError(fmt.Errorf("mpc_verify: data cannot be empty"))
|
||||
return
|
||||
}
|
||||
if len(signature) == 0 {
|
||||
ctx.ResultError(fmt.Errorf("mpc_verify: signature cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
pubKeyBytes, err := hex.DecodeString(pubKeyHex)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_verify: invalid public key hex: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := mpc.VerifyWithPubKey(pubKeyBytes, data, signature)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_verify: verification error: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if valid {
|
||||
ctx.ResultInt(1)
|
||||
} else {
|
||||
ctx.ResultInt(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func registerRefreshFunction(conn *sqlite3.Conn) error {
|
||||
return conn.CreateFunction("mpc_refresh", 1, 0, func(ctx sqlite3.Context, args ...sqlite3.Value) {
|
||||
if len(args) != 1 {
|
||||
ctx.ResultError(fmt.Errorf("mpc_refresh requires 1 argument: enclave_id"))
|
||||
return
|
||||
}
|
||||
|
||||
enclaveID := args[0].Text()
|
||||
if enclaveID == "" {
|
||||
ctx.ResultError(fmt.Errorf("mpc_refresh: enclave_id cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
enclave, err := loadSimpleEnclaveFromDB(enclaveID)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_refresh: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
newEnclave, err := enclave.Refresh()
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_refresh: refresh failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSimpleEnclaveInDB(enclaveID, newEnclave); err != nil {
|
||||
ctx.ResultError(fmt.Errorf("mpc_refresh: update failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ResultText(enclaveID)
|
||||
})
|
||||
}
|
||||
|
||||
func loadSimpleEnclaveFromDB(enclaveID string) (*mpc.SimpleEnclave, error) {
|
||||
kb := Get()
|
||||
if kb == nil {
|
||||
return nil, fmt.Errorf("keybase not initialized")
|
||||
}
|
||||
|
||||
dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave not found: %w", err)
|
||||
}
|
||||
|
||||
return mpc.ImportSimpleEnclave(
|
||||
dbEnc.PublicKey,
|
||||
dbEnc.ValShare,
|
||||
dbEnc.UserShare,
|
||||
dbEnc.Nonce,
|
||||
mpc.CurveName(dbEnc.Curve),
|
||||
)
|
||||
}
|
||||
|
||||
func updateSimpleEnclaveInDB(enclaveID string, enclave *mpc.SimpleEnclave) error {
|
||||
kb := Get()
|
||||
if kb == nil {
|
||||
return fmt.Errorf("keybase not initialized")
|
||||
}
|
||||
|
||||
dbEnc, err := kb.queries.GetEnclaveByID(context.Background(), enclaveID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get enclave: %w", err)
|
||||
}
|
||||
|
||||
_, err = kb.db.ExecContext(context.Background(), `
|
||||
UPDATE mpc_enclaves
|
||||
SET val_share = ?, user_share = ?, nonce = ?, rotated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`, enclave.GetShare1(), enclave.GetShare2(), enclave.GetNonce(), dbEnc.ID)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type Account struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
KeyShareID int64 `json:"key_share_id"`
|
||||
EnclaveID int64 `json:"enclave_id"`
|
||||
Address string `json:"address"`
|
||||
ChainID string `json:"chain_id"`
|
||||
CoinType int64 `json:"coin_type"`
|
||||
@@ -40,22 +40,6 @@ type Credential struct {
|
||||
LastUsed string `json:"last_used"`
|
||||
}
|
||||
|
||||
type Delegation struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
UcanID int64 `json:"ucan_id"`
|
||||
Delegator string `json:"delegator"`
|
||||
Delegate string `json:"delegate"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
Caveats json.RawMessage `json:"caveats"`
|
||||
ParentID *int64 `json:"parent_id"`
|
||||
Depth int64 `json:"depth"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
}
|
||||
|
||||
type DidDocument struct {
|
||||
ID int64 `json:"id"`
|
||||
Did string `json:"did"`
|
||||
@@ -71,7 +55,7 @@ type Grant struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
ServiceID int64 `json:"service_id"`
|
||||
UcanID *int64 `json:"ucan_id"`
|
||||
DelegationCid *string `json:"delegation_cid"`
|
||||
Scopes json.RawMessage `json:"scopes"`
|
||||
Accounts json.RawMessage `json:"accounts"`
|
||||
Status string `json:"status"`
|
||||
@@ -80,19 +64,16 @@ type Grant struct {
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
}
|
||||
|
||||
type KeyShare struct {
|
||||
type MpcEnclafe struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
ShareID string `json:"share_id"`
|
||||
KeyID string `json:"key_id"`
|
||||
PartyIndex int64 `json:"party_index"`
|
||||
Threshold int64 `json:"threshold"`
|
||||
TotalParties int64 `json:"total_parties"`
|
||||
EnclaveID string `json:"enclave_id"`
|
||||
PublicKeyHex string `json:"public_key_hex"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
ValShare []byte `json:"val_share"`
|
||||
UserShare []byte `json:"user_share"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
Curve string `json:"curve"`
|
||||
ShareData string `json:"share_data"`
|
||||
PublicKey string `json:"public_key"`
|
||||
ChainCode *string `json:"chain_code"`
|
||||
DerivationPath *string `json:"derivation_path"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
RotatedAt *string `json:"rotated_at"`
|
||||
@@ -131,33 +112,128 @@ type SyncCheckpoint struct {
|
||||
LastSynced string `json:"last_synced"`
|
||||
}
|
||||
|
||||
type UcanDelegation struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
Cid string `json:"cid"`
|
||||
Envelope []byte `json:"envelope"`
|
||||
Iss string `json:"iss"`
|
||||
Aud string `json:"aud"`
|
||||
Sub *string `json:"sub"`
|
||||
Cmd string `json:"cmd"`
|
||||
Pol *string `json:"pol"`
|
||||
Nbf *string `json:"nbf"`
|
||||
Exp *string `json:"exp"`
|
||||
IsRoot int64 `json:"is_root"`
|
||||
IsPowerline int64 `json:"is_powerline"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type UcanInvocation struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
Cid string `json:"cid"`
|
||||
Envelope []byte `json:"envelope"`
|
||||
Iss string `json:"iss"`
|
||||
Sub string `json:"sub"`
|
||||
Aud *string `json:"aud"`
|
||||
Cmd string `json:"cmd"`
|
||||
Prf string `json:"prf"`
|
||||
Exp *string `json:"exp"`
|
||||
Iat *string `json:"iat"`
|
||||
ExecutedAt *string `json:"executed_at"`
|
||||
ResultCid *string `json:"result_cid"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type UcanRevocation struct {
|
||||
ID int64 `json:"id"`
|
||||
UcanCid string `json:"ucan_cid"`
|
||||
DelegationCid string `json:"delegation_cid"`
|
||||
RevokedBy string `json:"revoked_by"`
|
||||
InvocationCid *string `json:"invocation_cid"`
|
||||
Reason *string `json:"reason"`
|
||||
RevokedAt string `json:"revoked_at"`
|
||||
}
|
||||
|
||||
type UcanToken struct {
|
||||
type VAccount struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
EnclaveID int64 `json:"enclave_id"`
|
||||
Address string `json:"address"`
|
||||
ChainID string `json:"chain_id"`
|
||||
CoinType int64 `json:"coin_type"`
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
AddressIndex int64 `json:"address_index"`
|
||||
Label *string `json:"label"`
|
||||
IsDefault int64 `json:"is_default"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PublicKeyHex string `json:"public_key_hex"`
|
||||
Curve string `json:"curve"`
|
||||
EnclaveRef string `json:"enclave_ref"`
|
||||
}
|
||||
|
||||
type VActiveDelegation struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
Cid string `json:"cid"`
|
||||
Issuer string `json:"issuer"`
|
||||
Audience string `json:"audience"`
|
||||
Subject *string `json:"subject"`
|
||||
Capabilities json.RawMessage `json:"capabilities"`
|
||||
ProofChain json.RawMessage `json:"proof_chain"`
|
||||
NotBefore *string `json:"not_before"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Nonce *string `json:"nonce"`
|
||||
Facts json.RawMessage `json:"facts"`
|
||||
Signature string `json:"signature"`
|
||||
RawToken string `json:"raw_token"`
|
||||
IsRevoked int64 `json:"is_revoked"`
|
||||
Envelope []byte `json:"envelope"`
|
||||
Iss string `json:"iss"`
|
||||
Aud string `json:"aud"`
|
||||
Sub *string `json:"sub"`
|
||||
Cmd string `json:"cmd"`
|
||||
Pol *string `json:"pol"`
|
||||
Nbf *string `json:"nbf"`
|
||||
Exp *string `json:"exp"`
|
||||
IsRoot int64 `json:"is_root"`
|
||||
IsPowerline int64 `json:"is_powerline"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type VActiveEnclafe struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
EnclaveID string `json:"enclave_id"`
|
||||
PublicKeyHex string `json:"public_key_hex"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
ValShare []byte `json:"val_share"`
|
||||
UserShare []byte `json:"user_share"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
Curve string `json:"curve"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
RotatedAt *string `json:"rotated_at"`
|
||||
}
|
||||
|
||||
type VGrant struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
ServiceID int64 `json:"service_id"`
|
||||
DelegationCid *string `json:"delegation_cid"`
|
||||
Scopes string `json:"scopes"`
|
||||
Accounts string `json:"accounts"`
|
||||
Status string `json:"status"`
|
||||
GrantedAt string `json:"granted_at"`
|
||||
LastUsed *string `json:"last_used"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
ServiceName string `json:"service_name"`
|
||||
ServiceOrigin string `json:"service_origin"`
|
||||
ServiceLogo *string `json:"service_logo"`
|
||||
}
|
||||
|
||||
type VSession struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
CredentialID int64 `json:"credential_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
DeviceInfo *string `json:"device_info"`
|
||||
IsCurrent int64 `json:"is_current"`
|
||||
LastActivity string `json:"last_activity"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DeviceName string `json:"device_name"`
|
||||
Authenticator *string `json:"authenticator"`
|
||||
}
|
||||
|
||||
type VerificationMethod struct {
|
||||
ID int64 `json:"id"`
|
||||
DidID int64 `json:"did_id"`
|
||||
|
||||
@@ -9,25 +9,30 @@ import (
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
ArchiveKeyShare(ctx context.Context, id int64) error
|
||||
CleanExpiredUCANs(ctx context.Context) error
|
||||
ArchiveEnclave(ctx context.Context, id int64) error
|
||||
CleanExpiredDelegations(ctx context.Context) error
|
||||
CleanOldInvocations(ctx context.Context) error
|
||||
CountActiveGrants(ctx context.Context, didID int64) (int64, error)
|
||||
CountCredentialsByDID(ctx context.Context, didID int64) (int64, error)
|
||||
CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error)
|
||||
CreateCredential(ctx context.Context, arg CreateCredentialParams) (Credential, error)
|
||||
CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocument, error)
|
||||
CreateDelegation(ctx context.Context, arg CreateDelegationParams) (Delegation, error)
|
||||
CreateDelegation(ctx context.Context, arg CreateDelegationParams) (UcanDelegation, error)
|
||||
CreateEnclave(ctx context.Context, arg CreateEnclaveParams) (MpcEnclafe, error)
|
||||
CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error)
|
||||
CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error)
|
||||
CreateInvocation(ctx context.Context, arg CreateInvocationParams) (UcanInvocation, error)
|
||||
// =============================================================================
|
||||
// UCAN REVOCATION QUERIES
|
||||
// =============================================================================
|
||||
CreateRevocation(ctx context.Context, arg CreateRevocationParams) error
|
||||
CreateService(ctx context.Context, arg CreateServiceParams) (Service, error)
|
||||
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
|
||||
CreateUCAN(ctx context.Context, arg CreateUCANParams) (UcanToken, error)
|
||||
CreateVerificationMethod(ctx context.Context, arg CreateVerificationMethodParams) (VerificationMethod, error)
|
||||
DeleteAccount(ctx context.Context, arg DeleteAccountParams) error
|
||||
DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error
|
||||
DeleteDelegation(ctx context.Context, arg DeleteDelegationParams) error
|
||||
DeleteEnclave(ctx context.Context, arg DeleteEnclaveParams) error
|
||||
DeleteExpiredSessions(ctx context.Context) error
|
||||
DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error
|
||||
DeleteSession(ctx context.Context, id int64) error
|
||||
DeleteVerificationMethod(ctx context.Context, id int64) error
|
||||
GetAccountByAddress(ctx context.Context, address string) (Account, error)
|
||||
@@ -39,10 +44,20 @@ type Querier interface {
|
||||
GetDIDByDID(ctx context.Context, did string) (DidDocument, error)
|
||||
GetDIDByID(ctx context.Context, id int64) (DidDocument, error)
|
||||
GetDefaultAccount(ctx context.Context, arg GetDefaultAccountParams) (Account, error)
|
||||
GetDelegationChain(ctx context.Context, arg GetDelegationChainParams) ([]Delegation, error)
|
||||
// =============================================================================
|
||||
// UCAN DELEGATION QUERIES (v1.0.0-rc.1)
|
||||
// =============================================================================
|
||||
GetDelegationByCID(ctx context.Context, cid string) (UcanDelegation, error)
|
||||
GetDelegationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error)
|
||||
GetEnclaveByID(ctx context.Context, enclaveID string) (MpcEnclafe, error)
|
||||
GetEnclaveByPubKeyHex(ctx context.Context, publicKeyHex string) (MpcEnclafe, error)
|
||||
GetGrantByService(ctx context.Context, arg GetGrantByServiceParams) (Grant, error)
|
||||
GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error)
|
||||
GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error)
|
||||
// =============================================================================
|
||||
// UCAN INVOCATION QUERIES (v1.0.0-rc.1)
|
||||
// =============================================================================
|
||||
GetInvocationByCID(ctx context.Context, cid string) (UcanInvocation, error)
|
||||
GetInvocationEnvelopeByCID(ctx context.Context, cid string) ([]byte, error)
|
||||
GetRevocation(ctx context.Context, delegationCid string) (UcanRevocation, error)
|
||||
GetServiceByID(ctx context.Context, id int64) (Service, error)
|
||||
// =============================================================================
|
||||
// SERVICE QUERIES
|
||||
@@ -53,55 +68,54 @@ type Querier interface {
|
||||
// SYNC QUERIES
|
||||
// =============================================================================
|
||||
GetSyncCheckpoint(ctx context.Context, arg GetSyncCheckpointParams) (SyncCheckpoint, error)
|
||||
GetUCANByCID(ctx context.Context, cid string) (UcanToken, error)
|
||||
GetVerificationMethod(ctx context.Context, arg GetVerificationMethodParams) (VerificationMethod, error)
|
||||
IsUCANRevoked(ctx context.Context, ucanCid string) (int64, error)
|
||||
IsDelegationRevoked(ctx context.Context, delegationCid string) (int64, error)
|
||||
ListAccountsByChain(ctx context.Context, arg ListAccountsByChainParams) ([]Account, error)
|
||||
// =============================================================================
|
||||
// ACCOUNT QUERIES
|
||||
// =============================================================================
|
||||
ListAccountsByDID(ctx context.Context, didID int64) ([]ListAccountsByDIDRow, error)
|
||||
ListAccountsByDID(ctx context.Context, didID int64) ([]VAccount, error)
|
||||
ListAllDIDs(ctx context.Context) ([]DidDocument, error)
|
||||
// =============================================================================
|
||||
// CREDENTIAL QUERIES
|
||||
// =============================================================================
|
||||
ListCredentialsByDID(ctx context.Context, didID int64) ([]Credential, error)
|
||||
ListDelegationsByDelegate(ctx context.Context, delegate string) ([]Delegation, error)
|
||||
ListDelegationsByAudience(ctx context.Context, aud string) ([]UcanDelegation, error)
|
||||
ListDelegationsByDID(ctx context.Context, didID int64) ([]UcanDelegation, error)
|
||||
ListDelegationsByIssuer(ctx context.Context, iss string) ([]UcanDelegation, error)
|
||||
ListDelegationsBySubject(ctx context.Context, sub *string) ([]UcanDelegation, error)
|
||||
ListDelegationsForCommand(ctx context.Context, arg ListDelegationsForCommandParams) ([]UcanDelegation, error)
|
||||
// =============================================================================
|
||||
// DELEGATION QUERIES
|
||||
// MPC ENCLAVE QUERIES
|
||||
// =============================================================================
|
||||
ListDelegationsByDelegator(ctx context.Context, delegator string) ([]Delegation, error)
|
||||
ListDelegationsForResource(ctx context.Context, arg ListDelegationsForResourceParams) ([]Delegation, error)
|
||||
ListEnclavesByDID(ctx context.Context, didID int64) ([]MpcEnclafe, error)
|
||||
// =============================================================================
|
||||
// GRANT QUERIES
|
||||
// =============================================================================
|
||||
ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error)
|
||||
// =============================================================================
|
||||
// KEY SHARE QUERIES
|
||||
// =============================================================================
|
||||
ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error)
|
||||
ListGrantsByDID(ctx context.Context, didID int64) ([]VGrant, error)
|
||||
ListInvocationsByDID(ctx context.Context, arg ListInvocationsByDIDParams) ([]UcanInvocation, error)
|
||||
ListInvocationsByIssuer(ctx context.Context, arg ListInvocationsByIssuerParams) ([]UcanInvocation, error)
|
||||
ListInvocationsBySubject(ctx context.Context, arg ListInvocationsBySubjectParams) ([]UcanInvocation, error)
|
||||
ListInvocationsForCommand(ctx context.Context, arg ListInvocationsForCommandParams) ([]UcanInvocation, error)
|
||||
ListPendingInvocations(ctx context.Context, didID int64) ([]UcanInvocation, error)
|
||||
ListPowerlineDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error)
|
||||
ListRevocationsByRevoker(ctx context.Context, revokedBy string) ([]UcanRevocation, error)
|
||||
ListRootDelegations(ctx context.Context, didID int64) ([]UcanDelegation, error)
|
||||
// =============================================================================
|
||||
// SESSION QUERIES
|
||||
// =============================================================================
|
||||
ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error)
|
||||
ListSessionsByDID(ctx context.Context, didID int64) ([]VSession, error)
|
||||
ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncCheckpoint, error)
|
||||
ListUCANsByAudience(ctx context.Context, audience string) ([]UcanToken, error)
|
||||
// =============================================================================
|
||||
// UCAN TOKEN QUERIES
|
||||
// =============================================================================
|
||||
ListUCANsByDID(ctx context.Context, didID int64) ([]UcanToken, error)
|
||||
// =============================================================================
|
||||
// VERIFICATION METHOD QUERIES
|
||||
// =============================================================================
|
||||
ListVerificationMethods(ctx context.Context, didID int64) ([]VerificationMethod, error)
|
||||
ListVerifiedServices(ctx context.Context) ([]Service, error)
|
||||
MarkInvocationExecuted(ctx context.Context, arg MarkInvocationExecutedParams) error
|
||||
ReactivateGrant(ctx context.Context, id int64) error
|
||||
RenameCredential(ctx context.Context, arg RenameCredentialParams) error
|
||||
RevokeDelegation(ctx context.Context, id int64) error
|
||||
RevokeDelegationChain(ctx context.Context, arg RevokeDelegationChainParams) error
|
||||
RevokeGrant(ctx context.Context, id int64) error
|
||||
RevokeUCAN(ctx context.Context, cid string) error
|
||||
RotateKeyShare(ctx context.Context, id int64) error
|
||||
RotateEnclave(ctx context.Context, id int64) error
|
||||
SetCurrentSession(ctx context.Context, arg SetCurrentSessionParams) error
|
||||
SetDefaultAccount(ctx context.Context, arg SetDefaultAccountParams) error
|
||||
SuspendGrant(ctx context.Context, id int64) error
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
211
internal/keybase/store.go
Normal file
211
internal/keybase/store.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"enclave/internal/migrations"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type Keybase struct {
|
||||
db *sql.DB
|
||||
conn *sqlite3.Conn
|
||||
queries *Queries
|
||||
did string
|
||||
didID int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
instance *Keybase
|
||||
initMu sync.Mutex
|
||||
)
|
||||
|
||||
func Open() (*Keybase, error) {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
|
||||
if instance != nil {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
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("open database: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(migrations.SchemaSQL); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("init schema: %w", err)
|
||||
}
|
||||
|
||||
instance = &Keybase{
|
||||
db: db,
|
||||
conn: rawConn,
|
||||
queries: New(db),
|
||||
}
|
||||
|
||||
if rawConn != nil {
|
||||
if err := RegisterMPCFunctions(rawConn); err != nil {
|
||||
db.Close()
|
||||
instance = nil
|
||||
return nil, fmt.Errorf("register mpc functions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func Get() *Keybase {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
return instance
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
initMu.Lock()
|
||||
defer initMu.Unlock()
|
||||
|
||||
if instance == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := instance.db.Close()
|
||||
instance = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *Keybase) DB() *sql.DB {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.db
|
||||
}
|
||||
|
||||
func (k *Keybase) Queries() *Queries {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.queries
|
||||
}
|
||||
|
||||
func (k *Keybase) DID() string {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.did
|
||||
}
|
||||
|
||||
func (k *Keybase) DIDID() int64 {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.didID
|
||||
}
|
||||
|
||||
func (k *Keybase) SetDID(did string, didID int64) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.did = did
|
||||
k.didID = didID
|
||||
}
|
||||
|
||||
func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (string, error) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16])
|
||||
docJSON, _ := json.Marshal(map[string]any{
|
||||
"@context": []string{"https://www.w3.org/ns/did/v1"},
|
||||
"id": did,
|
||||
})
|
||||
|
||||
doc, err := k.queries.CreateDID(ctx, CreateDIDParams{
|
||||
Did: did,
|
||||
Controller: did,
|
||||
Document: docJSON,
|
||||
Sequence: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create DID: %w", err)
|
||||
}
|
||||
|
||||
k.did = did
|
||||
k.didID = doc.ID
|
||||
|
||||
return did, nil
|
||||
}
|
||||
|
||||
func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) {
|
||||
if len(data) < 100 {
|
||||
return "", fmt.Errorf("invalid database format")
|
||||
}
|
||||
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
if k.conn == nil {
|
||||
return "", fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
if err := serdes.Deserialize(k.conn, "main", data); err != nil {
|
||||
return "", fmt.Errorf("deserialize database: %w", err)
|
||||
}
|
||||
|
||||
docs, err := k.queries.ListAllDIDs(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("list DIDs: %w", err)
|
||||
}
|
||||
|
||||
if len(docs) == 0 {
|
||||
return "", fmt.Errorf("no DID found in database")
|
||||
}
|
||||
|
||||
k.did = docs[0].Did
|
||||
k.didID = docs[0].ID
|
||||
|
||||
return k.did, nil
|
||||
}
|
||||
|
||||
func (k *Keybase) Serialize() ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
if k.conn == nil {
|
||||
return nil, fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
return serdes.Serialize(k.conn, "main")
|
||||
}
|
||||
|
||||
func (k *Keybase) WithTx(ctx context.Context, fn func(*Queries) error) error {
|
||||
tx, err := k.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
|
||||
if err := fn(k.queries.WithTx(tx)); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -72,47 +72,42 @@ DELETE FROM credentials WHERE id = ? AND did_id = ?;
|
||||
SELECT COUNT(*) FROM credentials WHERE did_id = ?;
|
||||
|
||||
-- =============================================================================
|
||||
-- KEY SHARE QUERIES
|
||||
-- MPC ENCLAVE QUERIES
|
||||
-- =============================================================================
|
||||
|
||||
-- name: ListKeySharesByDID :many
|
||||
SELECT * FROM key_shares WHERE did_id = ? AND status = 'active' ORDER BY created_at;
|
||||
-- name: ListEnclavesByDID :many
|
||||
SELECT * FROM mpc_enclaves WHERE did_id = ? AND status = 'active' ORDER BY created_at;
|
||||
|
||||
-- name: GetKeyShareByID :one
|
||||
SELECT * FROM key_shares WHERE share_id = ? LIMIT 1;
|
||||
-- name: GetEnclaveByID :one
|
||||
SELECT * FROM mpc_enclaves WHERE enclave_id = ? LIMIT 1;
|
||||
|
||||
-- name: GetKeyShareByKeyID :one
|
||||
SELECT * FROM key_shares WHERE did_id = ? AND key_id = ? AND status = 'active' LIMIT 1;
|
||||
-- name: GetEnclaveByPubKeyHex :one
|
||||
SELECT * FROM mpc_enclaves WHERE public_key_hex = ? LIMIT 1;
|
||||
|
||||
-- name: CreateKeyShare :one
|
||||
INSERT INTO key_shares (
|
||||
did_id, share_id, key_id, party_index, threshold, total_parties,
|
||||
curve, share_data, public_key, chain_code, derivation_path
|
||||
-- name: CreateEnclave :one
|
||||
INSERT INTO mpc_enclaves (
|
||||
did_id, enclave_id, public_key_hex, public_key, val_share, user_share, nonce, curve
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: RotateKeyShare :exec
|
||||
UPDATE key_shares
|
||||
-- name: RotateEnclave :exec
|
||||
UPDATE mpc_enclaves
|
||||
SET status = 'rotating', rotated_at = datetime('now')
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ArchiveKeyShare :exec
|
||||
UPDATE key_shares SET status = 'archived' WHERE id = ?;
|
||||
-- name: ArchiveEnclave :exec
|
||||
UPDATE mpc_enclaves SET status = 'archived' WHERE id = ?;
|
||||
|
||||
-- name: DeleteKeyShare :exec
|
||||
DELETE FROM key_shares WHERE id = ? AND did_id = ?;
|
||||
-- name: DeleteEnclave :exec
|
||||
DELETE FROM mpc_enclaves WHERE id = ? AND did_id = ?;
|
||||
|
||||
-- =============================================================================
|
||||
-- ACCOUNT QUERIES
|
||||
-- =============================================================================
|
||||
|
||||
-- name: ListAccountsByDID :many
|
||||
SELECT a.*, k.public_key, k.curve
|
||||
FROM accounts a
|
||||
JOIN key_shares k ON a.key_share_id = k.id
|
||||
WHERE a.did_id = ?
|
||||
ORDER BY a.is_default DESC, a.created_at;
|
||||
SELECT * FROM v_accounts WHERE did_id = ? ORDER BY is_default DESC, created_at;
|
||||
|
||||
-- name: ListAccountsByChain :many
|
||||
SELECT * FROM accounts WHERE did_id = ? AND chain_id = ? ORDER BY account_index, address_index;
|
||||
@@ -124,7 +119,7 @@ SELECT * FROM accounts WHERE address = ? LIMIT 1;
|
||||
SELECT * FROM accounts WHERE did_id = ? AND chain_id = ? AND is_default = 1 LIMIT 1;
|
||||
|
||||
-- name: CreateAccount :one
|
||||
INSERT INTO accounts (did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label)
|
||||
INSERT INTO accounts (did_id, enclave_id, address, chain_id, coin_type, account_index, address_index, label)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
@@ -140,53 +135,144 @@ UPDATE accounts SET label = ? WHERE id = ?;
|
||||
DELETE FROM accounts WHERE id = ? AND did_id = ?;
|
||||
|
||||
-- =============================================================================
|
||||
-- UCAN TOKEN QUERIES
|
||||
-- UCAN DELEGATION QUERIES (v1.0.0-rc.1)
|
||||
-- =============================================================================
|
||||
|
||||
-- name: ListUCANsByDID :many
|
||||
SELECT * FROM ucan_tokens
|
||||
WHERE did_id = ? AND is_revoked = 0 AND expires_at > datetime('now')
|
||||
ORDER BY created_at DESC;
|
||||
-- name: GetDelegationByCID :one
|
||||
SELECT * FROM ucan_delegations WHERE cid = ? LIMIT 1;
|
||||
|
||||
-- name: ListUCANsByAudience :many
|
||||
SELECT * FROM ucan_tokens
|
||||
WHERE audience = ? AND is_revoked = 0 AND expires_at > datetime('now')
|
||||
ORDER BY created_at DESC;
|
||||
-- name: GetDelegationEnvelopeByCID :one
|
||||
SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1;
|
||||
|
||||
-- name: GetUCANByCID :one
|
||||
SELECT * FROM ucan_tokens WHERE cid = ? LIMIT 1;
|
||||
|
||||
-- name: CreateUCAN :one
|
||||
INSERT INTO ucan_tokens (
|
||||
did_id, cid, issuer, audience, subject, capabilities,
|
||||
proof_chain, not_before, expires_at, nonce, facts, signature, raw_token
|
||||
-- name: CreateDelegation :one
|
||||
INSERT INTO ucan_delegations (
|
||||
did_id, cid, envelope, iss, aud, sub, cmd, pol, nbf, exp, is_root, is_powerline
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: RevokeUCAN :exec
|
||||
UPDATE ucan_tokens SET is_revoked = 1 WHERE cid = ?;
|
||||
-- name: ListDelegationsByDID :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE did_id = ? AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: IsUCANRevoked :one
|
||||
SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE ucan_cid = ?) as revoked;
|
||||
-- name: ListDelegationsByIssuer :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE iss = ? AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListDelegationsByAudience :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE aud = ? AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListDelegationsBySubject :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE sub = ? AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListDelegationsForCommand :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE did_id = ?
|
||||
AND (cmd = ? OR cmd = '/' OR ? LIKE cmd || '/%')
|
||||
AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListRootDelegations :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE did_id = ? AND is_root = 1 AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListPowerlineDelegations :many
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE did_id = ? AND is_powerline = 1 AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: DeleteDelegation :exec
|
||||
DELETE FROM ucan_delegations WHERE cid = ? AND did_id = ?;
|
||||
|
||||
-- name: CleanExpiredDelegations :exec
|
||||
DELETE FROM ucan_delegations WHERE exp < datetime('now', '-30 days');
|
||||
|
||||
-- =============================================================================
|
||||
-- UCAN INVOCATION QUERIES (v1.0.0-rc.1)
|
||||
-- =============================================================================
|
||||
|
||||
-- name: GetInvocationByCID :one
|
||||
SELECT * FROM ucan_invocations WHERE cid = ? LIMIT 1;
|
||||
|
||||
-- name: GetInvocationEnvelopeByCID :one
|
||||
SELECT envelope FROM ucan_invocations WHERE cid = ? LIMIT 1;
|
||||
|
||||
-- name: CreateInvocation :one
|
||||
INSERT INTO ucan_invocations (
|
||||
did_id, cid, envelope, iss, sub, aud, cmd, prf, exp, iat
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListInvocationsByDID :many
|
||||
SELECT * FROM ucan_invocations
|
||||
WHERE did_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?;
|
||||
|
||||
-- name: ListInvocationsByIssuer :many
|
||||
SELECT * FROM ucan_invocations
|
||||
WHERE iss = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?;
|
||||
|
||||
-- name: ListInvocationsBySubject :many
|
||||
SELECT * FROM ucan_invocations
|
||||
WHERE sub = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?;
|
||||
|
||||
-- name: ListInvocationsForCommand :many
|
||||
SELECT * FROM ucan_invocations
|
||||
WHERE did_id = ? AND cmd = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?;
|
||||
|
||||
-- name: MarkInvocationExecuted :exec
|
||||
UPDATE ucan_invocations
|
||||
SET executed_at = datetime('now'), result_cid = ?
|
||||
WHERE cid = ?;
|
||||
|
||||
-- name: ListPendingInvocations :many
|
||||
SELECT * FROM ucan_invocations
|
||||
WHERE did_id = ? AND executed_at IS NULL AND (exp IS NULL OR exp > datetime('now'))
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: CleanOldInvocations :exec
|
||||
DELETE FROM ucan_invocations WHERE created_at < datetime('now', '-90 days');
|
||||
|
||||
-- =============================================================================
|
||||
-- UCAN REVOCATION QUERIES
|
||||
-- =============================================================================
|
||||
|
||||
-- name: CreateRevocation :exec
|
||||
INSERT INTO ucan_revocations (ucan_cid, revoked_by, reason)
|
||||
VALUES (?, ?, ?);
|
||||
INSERT INTO ucan_revocations (delegation_cid, revoked_by, invocation_cid, reason)
|
||||
VALUES (?, ?, ?, ?);
|
||||
|
||||
-- name: CleanExpiredUCANs :exec
|
||||
DELETE FROM ucan_tokens WHERE expires_at < datetime('now', '-30 days');
|
||||
-- name: IsDelegationRevoked :one
|
||||
SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked;
|
||||
|
||||
-- name: GetRevocation :one
|
||||
SELECT * FROM ucan_revocations WHERE delegation_cid = ? LIMIT 1;
|
||||
|
||||
-- name: ListRevocationsByRevoker :many
|
||||
SELECT * FROM ucan_revocations
|
||||
WHERE revoked_by = ?
|
||||
ORDER BY revoked_at DESC;
|
||||
|
||||
-- =============================================================================
|
||||
-- SESSION QUERIES
|
||||
-- =============================================================================
|
||||
|
||||
-- name: ListSessionsByDID :many
|
||||
SELECT s.*, c.device_name, c.authenticator
|
||||
FROM sessions s
|
||||
JOIN credentials c ON s.credential_id = c.id
|
||||
WHERE s.did_id = ? AND s.expires_at > datetime('now')
|
||||
ORDER BY s.last_activity DESC;
|
||||
SELECT * FROM v_sessions WHERE did_id = ? ORDER BY last_activity DESC;
|
||||
|
||||
-- name: GetSessionByID :one
|
||||
SELECT * FROM sessions WHERE session_id = ? LIMIT 1;
|
||||
@@ -241,17 +327,13 @@ SELECT * FROM services WHERE is_verified = 1 ORDER BY name;
|
||||
-- =============================================================================
|
||||
|
||||
-- name: ListGrantsByDID :many
|
||||
SELECT g.*, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo
|
||||
FROM grants g
|
||||
JOIN services s ON g.service_id = s.id
|
||||
WHERE g.did_id = ? AND g.status = 'active'
|
||||
ORDER BY g.last_used DESC NULLS LAST;
|
||||
SELECT * FROM v_grants WHERE did_id = ? AND status = 'active' ORDER BY last_used DESC NULLS LAST;
|
||||
|
||||
-- name: GetGrantByService :one
|
||||
SELECT * FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1;
|
||||
|
||||
-- name: CreateGrant :one
|
||||
INSERT INTO grants (did_id, service_id, ucan_id, scopes, accounts, expires_at)
|
||||
INSERT INTO grants (did_id, service_id, delegation_cid, scopes, accounts, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
@@ -273,41 +355,6 @@ UPDATE grants SET status = 'active' WHERE id = ? AND status = 'suspended';
|
||||
-- name: CountActiveGrants :one
|
||||
SELECT COUNT(*) FROM grants WHERE did_id = ? AND status = 'active';
|
||||
|
||||
-- =============================================================================
|
||||
-- DELEGATION QUERIES
|
||||
-- =============================================================================
|
||||
|
||||
-- name: ListDelegationsByDelegator :many
|
||||
SELECT * FROM delegations
|
||||
WHERE delegator = ? AND status = 'active'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListDelegationsByDelegate :many
|
||||
SELECT * FROM delegations
|
||||
WHERE delegate = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > datetime('now'))
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListDelegationsForResource :many
|
||||
SELECT * FROM delegations
|
||||
WHERE did_id = ? AND resource = ? AND status = 'active'
|
||||
ORDER BY depth, created_at;
|
||||
|
||||
-- name: GetDelegationChain :many
|
||||
SELECT * FROM delegations WHERE id = ? OR parent_id = ? ORDER BY depth DESC;
|
||||
|
||||
-- name: CreateDelegation :one
|
||||
INSERT INTO delegations (
|
||||
did_id, ucan_id, delegator, delegate, resource, action, caveats, parent_id, depth, expires_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: RevokeDelegation :exec
|
||||
UPDATE delegations SET status = 'revoked' WHERE id = ?;
|
||||
|
||||
-- name: RevokeDelegationChain :exec
|
||||
UPDATE delegations SET status = 'revoked' WHERE id = ? OR parent_id = ?;
|
||||
|
||||
-- =============================================================================
|
||||
-- SYNC QUERIES
|
||||
-- =============================================================================
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
-- =============================================================================
|
||||
-- NEBULA KEY ENCLAVE SCHEMA
|
||||
-- Encrypted SQLite database for sensitive wallet data
|
||||
-- UCAN v1.0.0-rc.1 compliant
|
||||
-- =============================================================================
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -65,46 +66,41 @@ CREATE INDEX idx_credentials_did_id ON credentials(did_id);
|
||||
CREATE INDEX idx_credentials_credential_id ON credentials(credential_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- MPC KEY SHARES
|
||||
-- MPC ENCLAVES
|
||||
-- =============================================================================
|
||||
|
||||
-- Key Shares: MPC/TSS key share storage
|
||||
CREATE TABLE IF NOT EXISTS key_shares (
|
||||
CREATE TABLE IF NOT EXISTS mpc_enclaves (
|
||||
id INTEGER PRIMARY KEY,
|
||||
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
|
||||
share_id TEXT NOT NULL UNIQUE, -- Unique identifier for this share
|
||||
key_id TEXT NOT NULL, -- Identifier for the full key (shared across parties)
|
||||
party_index INTEGER NOT NULL, -- This party's index (1, 2, 3...)
|
||||
threshold INTEGER NOT NULL, -- Minimum shares needed to sign
|
||||
total_parties INTEGER NOT NULL, -- Total number of parties
|
||||
curve TEXT NOT NULL DEFAULT 'secp256k1', -- secp256k1, ed25519
|
||||
share_data TEXT NOT NULL, -- Encrypted key share (base64)
|
||||
public_key TEXT NOT NULL, -- Full public key (base64)
|
||||
chain_code TEXT, -- BIP32 chain code for derivation
|
||||
derivation_path TEXT, -- BIP44 path: m/44'/60'/0'/0
|
||||
enclave_id TEXT NOT NULL UNIQUE,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
public_key BLOB NOT NULL,
|
||||
val_share BLOB NOT NULL,
|
||||
user_share BLOB NOT NULL,
|
||||
nonce BLOB NOT NULL,
|
||||
curve TEXT NOT NULL DEFAULT 'secp256k1',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'rotating', 'archived')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
rotated_at TEXT,
|
||||
UNIQUE(did_id, key_id, party_index)
|
||||
UNIQUE(did_id, enclave_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_key_shares_did_id ON key_shares(did_id);
|
||||
CREATE INDEX idx_key_shares_key_id ON key_shares(key_id);
|
||||
CREATE INDEX idx_mpc_enclaves_did_id ON mpc_enclaves(did_id);
|
||||
CREATE INDEX idx_mpc_enclaves_public_key_hex ON mpc_enclaves(public_key_hex);
|
||||
|
||||
-- Derived Accounts: Wallet accounts derived from key shares
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
|
||||
key_share_id INTEGER NOT NULL REFERENCES key_shares(id) ON DELETE CASCADE,
|
||||
address TEXT NOT NULL, -- Derived address
|
||||
chain_id TEXT NOT NULL, -- sonr-mainnet-1, ethereum, etc.
|
||||
coin_type INTEGER NOT NULL, -- BIP44 coin type (118=cosmos, 60=eth)
|
||||
account_index INTEGER NOT NULL DEFAULT 0, -- BIP44 account index
|
||||
address_index INTEGER NOT NULL DEFAULT 0, -- BIP44 address index
|
||||
label TEXT DEFAULT '', -- User-assigned label
|
||||
enclave_id INTEGER NOT NULL REFERENCES mpc_enclaves(id) ON DELETE CASCADE,
|
||||
address TEXT NOT NULL,
|
||||
chain_id TEXT NOT NULL,
|
||||
coin_type INTEGER NOT NULL,
|
||||
account_index INTEGER NOT NULL DEFAULT 0,
|
||||
address_index INTEGER NOT NULL DEFAULT 0,
|
||||
label TEXT DEFAULT '',
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(key_share_id, chain_id, account_index, address_index)
|
||||
UNIQUE(enclave_id, chain_id, account_index, address_index)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_accounts_did_id ON accounts(did_id);
|
||||
@@ -112,44 +108,97 @@ CREATE INDEX idx_accounts_address ON accounts(address);
|
||||
CREATE INDEX idx_accounts_chain_id ON accounts(chain_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- UCAN AUTHORIZATION
|
||||
-- UCAN AUTHORIZATION (v1.0.0-rc.1)
|
||||
-- =============================================================================
|
||||
|
||||
-- UCAN Tokens: Capability authorization tokens
|
||||
CREATE TABLE IF NOT EXISTS ucan_tokens (
|
||||
-- UCAN Delegations: v1.0.0-rc.1 delegation envelopes
|
||||
-- Stores sealed DAG-CBOR envelopes with extracted fields for indexing
|
||||
CREATE TABLE IF NOT EXISTS ucan_delegations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
|
||||
cid TEXT NOT NULL UNIQUE, -- Content ID of UCAN (for dedup)
|
||||
issuer TEXT NOT NULL, -- iss: DID of issuer
|
||||
audience TEXT NOT NULL, -- aud: DID of recipient
|
||||
subject TEXT, -- sub: DID token is about (optional)
|
||||
capabilities TEXT NOT NULL, -- JSON array of capabilities
|
||||
proof_chain TEXT DEFAULT '[]', -- JSON array of parent UCAN CIDs
|
||||
not_before TEXT, -- nbf: validity start
|
||||
expires_at TEXT NOT NULL, -- exp: expiration time
|
||||
nonce TEXT, -- Replay protection
|
||||
facts TEXT DEFAULT '{}', -- Additional facts (JSON)
|
||||
signature TEXT NOT NULL, -- Base64 encoded signature
|
||||
raw_token TEXT NOT NULL, -- Full encoded UCAN token
|
||||
is_revoked INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Content Identifier (immutable, unique)
|
||||
cid TEXT NOT NULL UNIQUE,
|
||||
|
||||
-- Sealed envelope (DAG-CBOR encoded)
|
||||
envelope BLOB NOT NULL,
|
||||
|
||||
-- Extracted fields for indexing/queries
|
||||
iss TEXT NOT NULL, -- Issuer DID
|
||||
aud TEXT NOT NULL, -- Audience DID
|
||||
sub TEXT, -- Subject DID (null = powerline)
|
||||
cmd TEXT NOT NULL, -- Command (e.g., "/vault/read")
|
||||
|
||||
-- Policy stored as JSON for inspection
|
||||
pol TEXT DEFAULT '[]', -- Policy JSON
|
||||
|
||||
-- Temporal fields
|
||||
nbf TEXT, -- Not before (ISO8601)
|
||||
exp TEXT, -- Expiration (ISO8601, null = never)
|
||||
|
||||
-- Metadata
|
||||
is_root INTEGER NOT NULL DEFAULT 0, -- iss == sub
|
||||
is_powerline INTEGER NOT NULL DEFAULT 0, -- sub IS NULL
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ucan_tokens_did_id ON ucan_tokens(did_id);
|
||||
CREATE INDEX idx_ucan_tokens_issuer ON ucan_tokens(issuer);
|
||||
CREATE INDEX idx_ucan_tokens_audience ON ucan_tokens(audience);
|
||||
CREATE INDEX idx_ucan_tokens_expires_at ON ucan_tokens(expires_at);
|
||||
CREATE INDEX idx_ucan_delegations_cid ON ucan_delegations(cid);
|
||||
CREATE INDEX idx_ucan_delegations_did_id ON ucan_delegations(did_id);
|
||||
CREATE INDEX idx_ucan_delegations_iss ON ucan_delegations(iss);
|
||||
CREATE INDEX idx_ucan_delegations_aud ON ucan_delegations(aud);
|
||||
CREATE INDEX idx_ucan_delegations_sub ON ucan_delegations(sub);
|
||||
CREATE INDEX idx_ucan_delegations_cmd ON ucan_delegations(cmd);
|
||||
CREATE INDEX idx_ucan_delegations_exp ON ucan_delegations(exp);
|
||||
|
||||
-- UCAN Revocations: Revoked UCAN tokens
|
||||
-- UCAN Invocations: v1.0.0-rc.1 invocation envelopes (audit log)
|
||||
CREATE TABLE IF NOT EXISTS ucan_invocations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
|
||||
|
||||
-- Content Identifier
|
||||
cid TEXT NOT NULL UNIQUE,
|
||||
|
||||
-- Sealed envelope (DAG-CBOR encoded)
|
||||
envelope BLOB NOT NULL,
|
||||
|
||||
-- Extracted fields for indexing
|
||||
iss TEXT NOT NULL, -- Invoker DID
|
||||
sub TEXT NOT NULL, -- Subject DID
|
||||
aud TEXT, -- Executor DID (if different from sub)
|
||||
cmd TEXT NOT NULL, -- Command invoked
|
||||
|
||||
-- Proof chain (JSON array of delegation CIDs)
|
||||
prf TEXT NOT NULL DEFAULT '[]',
|
||||
|
||||
-- Temporal
|
||||
exp TEXT, -- Expiration
|
||||
iat TEXT, -- Issued at
|
||||
|
||||
-- Execution tracking
|
||||
executed_at TEXT, -- When actually executed
|
||||
result_cid TEXT, -- CID of receipt (if executed)
|
||||
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ucan_invocations_cid ON ucan_invocations(cid);
|
||||
CREATE INDEX idx_ucan_invocations_did_id ON ucan_invocations(did_id);
|
||||
CREATE INDEX idx_ucan_invocations_iss ON ucan_invocations(iss);
|
||||
CREATE INDEX idx_ucan_invocations_sub ON ucan_invocations(sub);
|
||||
CREATE INDEX idx_ucan_invocations_cmd ON ucan_invocations(cmd);
|
||||
|
||||
-- UCAN Revocations: Track revoked delegations
|
||||
CREATE TABLE IF NOT EXISTS ucan_revocations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ucan_cid TEXT NOT NULL UNIQUE, -- CID of revoked UCAN
|
||||
revoked_by TEXT NOT NULL, -- DID that revoked
|
||||
delegation_cid TEXT NOT NULL UNIQUE, -- CID of revoked delegation
|
||||
revoked_by TEXT NOT NULL, -- Revoker DID
|
||||
invocation_cid TEXT, -- CID of revocation invocation
|
||||
reason TEXT,
|
||||
revoked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ucan_revocations_cid ON ucan_revocations(ucan_cid);
|
||||
CREATE INDEX idx_ucan_revocations_delegation_cid ON ucan_revocations(delegation_cid);
|
||||
CREATE INDEX idx_ucan_revocations_revoked_by ON ucan_revocations(revoked_by);
|
||||
|
||||
-- =============================================================================
|
||||
-- DEVICE SESSIONS
|
||||
@@ -191,12 +240,12 @@ CREATE TABLE IF NOT EXISTS services (
|
||||
|
||||
CREATE INDEX idx_services_origin ON services(origin);
|
||||
|
||||
-- Grants: User grants to services
|
||||
-- Grants: User grants to services (backed by UCAN delegations)
|
||||
CREATE TABLE IF NOT EXISTS grants (
|
||||
id INTEGER PRIMARY KEY,
|
||||
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
ucan_id INTEGER REFERENCES ucan_tokens(id) ON DELETE SET NULL,
|
||||
delegation_cid TEXT REFERENCES ucan_delegations(cid) ON DELETE SET NULL, -- v1.0.0-rc.1 delegation
|
||||
scopes TEXT NOT NULL DEFAULT '[]', -- JSON array of granted scopes
|
||||
accounts TEXT NOT NULL DEFAULT '[]', -- JSON array of account IDs exposed
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'revoked')),
|
||||
@@ -209,32 +258,7 @@ CREATE TABLE IF NOT EXISTS grants (
|
||||
CREATE INDEX idx_grants_did_id ON grants(did_id);
|
||||
CREATE INDEX idx_grants_service_id ON grants(service_id);
|
||||
CREATE INDEX idx_grants_status ON grants(status);
|
||||
|
||||
-- =============================================================================
|
||||
-- CAPABILITY DELEGATIONS
|
||||
-- =============================================================================
|
||||
|
||||
-- Delegations: Capability delegation chains
|
||||
CREATE TABLE IF NOT EXISTS delegations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
|
||||
ucan_id INTEGER NOT NULL REFERENCES ucan_tokens(id) ON DELETE CASCADE,
|
||||
delegator TEXT NOT NULL, -- DID that delegated
|
||||
delegate TEXT NOT NULL, -- DID that received delegation
|
||||
resource TEXT NOT NULL, -- Resource URI (e.g., "sonr://vault/*")
|
||||
action TEXT NOT NULL, -- Action (e.g., "sign", "read", "write")
|
||||
caveats TEXT DEFAULT '{}', -- JSON: restrictions/conditions
|
||||
parent_id INTEGER REFERENCES delegations(id), -- Parent delegation (for chains)
|
||||
depth INTEGER NOT NULL DEFAULT 0, -- Delegation depth (0 = root)
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_delegations_did_id ON delegations(did_id);
|
||||
CREATE INDEX idx_delegations_delegator ON delegations(delegator);
|
||||
CREATE INDEX idx_delegations_delegate ON delegations(delegate);
|
||||
CREATE INDEX idx_delegations_resource ON delegations(resource);
|
||||
CREATE INDEX idx_grants_delegation_cid ON grants(delegation_cid);
|
||||
|
||||
-- =============================================================================
|
||||
-- SYNC STATE
|
||||
@@ -262,3 +286,40 @@ CREATE TRIGGER IF NOT EXISTS did_documents_updated_at
|
||||
BEGIN
|
||||
UPDATE did_documents SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
-- =============================================================================
|
||||
-- VIEWS (pre-computed JOINs for common queries)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_accounts AS
|
||||
SELECT
|
||||
a.id, a.did_id, a.enclave_id, a.address, a.chain_id,
|
||||
a.coin_type, a.account_index, a.address_index,
|
||||
a.label, a.is_default, a.created_at,
|
||||
e.public_key_hex, e.curve, e.enclave_id as enclave_ref
|
||||
FROM accounts a
|
||||
JOIN mpc_enclaves e ON a.enclave_id = e.id;
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_sessions AS
|
||||
SELECT
|
||||
s.id, s.did_id, s.credential_id, s.session_id, s.device_info,
|
||||
s.is_current, s.last_activity, s.expires_at, s.created_at,
|
||||
c.device_name, c.authenticator
|
||||
FROM sessions s
|
||||
JOIN credentials c ON s.credential_id = c.id
|
||||
WHERE s.expires_at > datetime('now');
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_grants AS
|
||||
SELECT
|
||||
g.id, g.did_id, g.service_id, g.delegation_cid, g.scopes,
|
||||
g.accounts, g.status, g.granted_at, g.last_used, g.expires_at,
|
||||
s.name as service_name, s.origin as service_origin, s.logo_url as service_logo
|
||||
FROM grants g
|
||||
JOIN services s ON g.service_id = s.id;
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_active_delegations AS
|
||||
SELECT * FROM ucan_delegations
|
||||
WHERE exp IS NULL OR exp > datetime('now');
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_active_enclaves AS
|
||||
SELECT * FROM mpc_enclaves WHERE status = 'active';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build wasip1
|
||||
|
||||
// Package state contains the state of the enclave.
|
||||
package state
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package types
|
||||
|
||||
// GenerateInput represents the input for the generate function
|
||||
type GenerateInput struct {
|
||||
Credential string `json:"credential"` // Base64-encoded PublicKeyCredential
|
||||
Credential string `json:"credential"`
|
||||
}
|
||||
|
||||
// GenerateOutput represents the output of the generate function
|
||||
type GenerateOutput struct {
|
||||
DID string `json:"did"`
|
||||
Database []byte `json:"database"`
|
||||
EnclaveID string `json:"enclave_id"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Accounts []AccountInfo `json:"accounts"`
|
||||
}
|
||||
|
||||
type AccountInfo struct {
|
||||
Address string `json:"address"`
|
||||
ChainID string `json:"chain_id"`
|
||||
CoinType int64 `json:"coin_type"`
|
||||
}
|
||||
|
||||
107
src/enclave.ts
107
src/enclave.ts
@@ -8,6 +8,31 @@ import type {
|
||||
Resource,
|
||||
} from './types';
|
||||
|
||||
function extractErrorMessage(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
if (typeof err === 'object' && err !== null) {
|
||||
const obj = err as Record<string, unknown>;
|
||||
if (typeof obj.message === 'string') return obj.message;
|
||||
if (typeof obj.error === 'string') return obj.error;
|
||||
if (typeof obj.msg === 'string') return obj.msg;
|
||||
const allKeys = [...Object.keys(obj), ...Object.getOwnPropertyNames(obj)];
|
||||
for (const key of allKeys) {
|
||||
const val = obj[key];
|
||||
if (typeof val === 'string' && val.length > 0) {
|
||||
return `${key}: ${val}`;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const str = JSON.stringify(err, Object.getOwnPropertyNames(err));
|
||||
if (str !== '{}') return str;
|
||||
} catch {}
|
||||
return `[error object with keys: ${allKeys.join(', ') || 'none'}]`;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
export class Enclave {
|
||||
private plugin: Plugin;
|
||||
private logger: EnclaveOptions['logger'];
|
||||
@@ -41,12 +66,28 @@ export class Enclave {
|
||||
this.log('generate: starting with credential');
|
||||
|
||||
const input = JSON.stringify({ credential });
|
||||
const result = await this.plugin.call('generate', input);
|
||||
if (!result) throw new Error('generate: plugin returned no output');
|
||||
const output = result.json() as GenerateOutput;
|
||||
|
||||
try {
|
||||
const result = await this.plugin.call('generate', input);
|
||||
if (!result) {
|
||||
throw new Error('generate: plugin returned no output');
|
||||
}
|
||||
const output = result.json() as GenerateOutput;
|
||||
this.log(`generate: created DID ${output.did}`);
|
||||
return output;
|
||||
} catch (err) {
|
||||
console.error('[DEBUG] Raw error:', err);
|
||||
console.error('[DEBUG] typeof:', typeof err);
|
||||
console.error('[DEBUG] constructor:', err?.constructor?.name);
|
||||
if (typeof err === 'object' && err !== null) {
|
||||
console.error('[DEBUG] keys:', Object.keys(err));
|
||||
console.error('[DEBUG] ownProps:', Object.getOwnPropertyNames(err));
|
||||
console.error('[DEBUG] prototype:', Object.getPrototypeOf(err));
|
||||
}
|
||||
const errMsg = extractErrorMessage(err);
|
||||
this.log(`generate: failed - ${errMsg}`, 'error');
|
||||
throw new Error(`generate: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async load(source: Uint8Array | number[]): Promise<LoadOutput> {
|
||||
@@ -59,38 +100,52 @@ export class Enclave {
|
||||
} else if (Array.isArray(source)) {
|
||||
database = source;
|
||||
} else {
|
||||
throw new Error('load: invalid source type');
|
||||
return { success: false, error: 'invalid source type' };
|
||||
}
|
||||
|
||||
const input = JSON.stringify({ database });
|
||||
const result = await this.plugin.call('load', input);
|
||||
if (!result) throw new Error('load: plugin returned no output');
|
||||
const output = result.json() as LoadOutput;
|
||||
|
||||
try {
|
||||
const result = await this.plugin.call('load', input);
|
||||
if (!result) {
|
||||
return { success: false, error: 'plugin returned no output' };
|
||||
}
|
||||
const output = result.json() as LoadOutput;
|
||||
if (output.success) {
|
||||
this.log(`load: loaded database for DID ${output.did}`);
|
||||
} else {
|
||||
this.log(`load: failed - ${output.error}`, 'error');
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (err) {
|
||||
const errMsg = extractErrorMessage(err);
|
||||
this.log(`load: failed - ${errMsg}`, 'error');
|
||||
return { success: false, error: errMsg };
|
||||
}
|
||||
}
|
||||
|
||||
async exec(filter: string, token?: string): Promise<ExecOutput> {
|
||||
this.log(`exec: executing filter "${filter}"`);
|
||||
|
||||
const input = JSON.stringify({ filter, token });
|
||||
const result = await this.plugin.call('exec', input);
|
||||
if (!result) throw new Error('exec: plugin returned no output');
|
||||
const output = result.json() as ExecOutput;
|
||||
|
||||
try {
|
||||
const result = await this.plugin.call('exec', input);
|
||||
if (!result) {
|
||||
return { success: false, error: 'plugin returned no output' };
|
||||
}
|
||||
const output = result.json() as ExecOutput;
|
||||
if (output.success) {
|
||||
this.log('exec: completed successfully');
|
||||
} else {
|
||||
this.log(`exec: failed - ${output.error}`, 'error');
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (err) {
|
||||
const errMsg = extractErrorMessage(err);
|
||||
this.log(`exec: failed - ${errMsg}`, 'error');
|
||||
return { success: false, error: errMsg };
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
@@ -109,24 +164,40 @@ export class Enclave {
|
||||
this.log(`query: resolving DID ${did || '(current)'}`);
|
||||
|
||||
const input = JSON.stringify({ did });
|
||||
const result = await this.plugin.call('query', input);
|
||||
if (!result) throw new Error('query: plugin returned no output');
|
||||
const output = result.json() as QueryOutput;
|
||||
|
||||
try {
|
||||
const result = await this.plugin.call('query', input);
|
||||
if (!result) {
|
||||
throw new Error('query: plugin returned no output');
|
||||
}
|
||||
const output = result.json() as QueryOutput;
|
||||
this.log(`query: resolved DID ${output.did}`);
|
||||
return output;
|
||||
} catch (err) {
|
||||
const errMsg = extractErrorMessage(err);
|
||||
this.log(`query: failed - ${errMsg}`, 'error');
|
||||
throw new Error(`query: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async ping(message: string = 'hello'): Promise<{ success: boolean; message: string; echo: string }> {
|
||||
this.log(`ping: sending "${message}"`);
|
||||
|
||||
const input = JSON.stringify({ message });
|
||||
const result = await this.plugin.call('ping', input);
|
||||
if (!result) throw new Error('ping: plugin returned no output');
|
||||
const output = result.json() as { success: boolean; message: string; echo: string };
|
||||
|
||||
try {
|
||||
const result = await this.plugin.call('ping', input);
|
||||
if (!result) {
|
||||
throw new Error('ping: plugin returned no output');
|
||||
}
|
||||
const output = result.json() as { success: boolean; message: string; echo: string };
|
||||
this.log(`ping: received ${output.success ? 'pong' : 'error'}`);
|
||||
return output;
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
this.log(`ping: failed - ${errMsg}`, 'error');
|
||||
throw new Error(`ping: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
|
||||
12
src/types.ts
12
src/types.ts
@@ -17,6 +17,18 @@ export interface GenerateOutput {
|
||||
did: string;
|
||||
/** Serialized database buffer for storage */
|
||||
database: number[];
|
||||
/** The MPC enclave ID */
|
||||
enclave_id?: string;
|
||||
/** The public key hex */
|
||||
public_key?: string;
|
||||
/** Default accounts created */
|
||||
accounts?: AccountInfo[];
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
address: string;
|
||||
chain_id: string;
|
||||
coin_type: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
592
src/ucan.ts
Normal file
592
src/ucan.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* UCAN Complete TypeScript Type Definitions
|
||||
* User-Controlled Authorization Network v1.0.0-rc.1
|
||||
*
|
||||
* Comprehensive types for Tokens, Delegation, and Invocation specifications
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// PRIMITIVES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Decentralized Identifier (DID) string
|
||||
* @see https://www.w3.org/TR/did-core/
|
||||
*/
|
||||
export type DID = `did:${string}:${string}`;
|
||||
|
||||
/**
|
||||
* IPLD Content Identifier in DAG-JSON format
|
||||
* CIDv1 with DAG-CBOR codec and SHA-256 multihash, base58btc encoded
|
||||
*/
|
||||
export interface CID {
|
||||
"/": string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary data in DAG-JSON format (base64 encoded)
|
||||
*/
|
||||
export interface Bytes {
|
||||
"/": {
|
||||
bytes: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix timestamp in seconds (53-bit integer for JS compatibility)
|
||||
* Range: -9007199254740991 to 9007199254740991
|
||||
*/
|
||||
export type Timestamp = number;
|
||||
|
||||
/**
|
||||
* Nullable timestamp - null indicates non-expiring
|
||||
*/
|
||||
export type NullableTimestamp = Timestamp | null;
|
||||
|
||||
/**
|
||||
* Varsig v1 header containing cryptographic algorithm metadata
|
||||
*/
|
||||
export type VarsigHeader = Bytes;
|
||||
|
||||
/**
|
||||
* Raw signature bytes
|
||||
*/
|
||||
export type Signature = Bytes;
|
||||
|
||||
// =============================================================================
|
||||
// COMMANDS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* UCAN Command - slash-delimited path describing the action
|
||||
* Must be lowercase, start with '/', no trailing slash
|
||||
*/
|
||||
export type Command =
|
||||
| "/"
|
||||
| `/${string}`
|
||||
| `/crud/${CRUDAction}`
|
||||
| `/msg/${MessageAction}`
|
||||
| `/ucan/${UCANAction}`
|
||||
| `/wasm/${WasmAction}`;
|
||||
|
||||
export type CRUDAction = "create" | "read" | "update" | "delete" | "mutate";
|
||||
export type MessageAction = "send" | "receive";
|
||||
export type UCANAction = "revoke";
|
||||
export type WasmAction = "run";
|
||||
|
||||
// =============================================================================
|
||||
// POLICY LANGUAGE
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* jq-inspired selector for navigating IPLD data
|
||||
* @example "." | ".foo" | ".bar[0]" | ".items[-1]" | ".optional?"
|
||||
*/
|
||||
export type Selector = string;
|
||||
|
||||
/**
|
||||
* Glob pattern for 'like' operator
|
||||
* Use * for wildcard, \* for literal asterisk
|
||||
*/
|
||||
export type GlobPattern = string;
|
||||
|
||||
// Policy Operators
|
||||
export type EqualityOperator = "==" | "!=";
|
||||
export type InequalityOperator = ">" | ">=" | "<" | "<=";
|
||||
export type ConnectiveOperator = "and" | "or";
|
||||
export type QuantifierOperator = "all" | "any";
|
||||
|
||||
// Policy Statements
|
||||
export type EqualityStatement = [EqualityOperator, Selector, unknown];
|
||||
export type InequalityStatement = [InequalityOperator, Selector, number];
|
||||
export type LikeStatement = ["like", Selector, GlobPattern];
|
||||
export type NotStatement = ["not", PolicyStatement];
|
||||
export type AndStatement = ["and", PolicyStatement[]];
|
||||
export type OrStatement = ["or", PolicyStatement[]];
|
||||
export type AllStatement = ["all", Selector, PolicyStatement];
|
||||
export type AnyStatement = ["any", Selector, PolicyStatement];
|
||||
|
||||
/**
|
||||
* Union of all policy statement types
|
||||
*/
|
||||
export type PolicyStatement =
|
||||
| EqualityStatement
|
||||
| InequalityStatement
|
||||
| LikeStatement
|
||||
| NotStatement
|
||||
| AndStatement
|
||||
| OrStatement
|
||||
| AllStatement
|
||||
| AnyStatement;
|
||||
|
||||
/**
|
||||
* UCAN Policy - array of statements forming implicit AND
|
||||
*/
|
||||
export type Policy = PolicyStatement[];
|
||||
|
||||
// =============================================================================
|
||||
// METADATA & ARGUMENTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Arbitrary metadata map
|
||||
*/
|
||||
export type Metadata = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Command arguments - shape defined by command type
|
||||
*/
|
||||
export type Arguments = Record<string, unknown>;
|
||||
|
||||
// =============================================================================
|
||||
// CAPABILITY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* UCAN Capability - the semantically-relevant claim of a delegation
|
||||
*/
|
||||
export interface Capability {
|
||||
/** Subject DID or null for powerline delegation */
|
||||
sub: DID | null;
|
||||
/** Command being delegated */
|
||||
cmd: Command;
|
||||
/** Policy constraints on invocation arguments */
|
||||
pol: Policy;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DELEGATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* UCAN Delegation Payload (ucan/dlg@1.0.0-rc.1)
|
||||
*/
|
||||
export interface DelegationPayload {
|
||||
/** Issuer DID - the delegator */
|
||||
iss: DID;
|
||||
/** Audience DID - the delegate */
|
||||
aud: DID;
|
||||
/** Subject DID or null for powerline */
|
||||
sub: DID | null;
|
||||
/** Command being delegated */
|
||||
cmd: Command;
|
||||
/** Policy constraints */
|
||||
pol: Policy;
|
||||
/** Random nonce for unique CID */
|
||||
nonce: Bytes;
|
||||
/** Optional metadata */
|
||||
meta?: Metadata;
|
||||
/** Not-before timestamp (optional) */
|
||||
nbf?: Timestamp;
|
||||
/** Expiration timestamp or null */
|
||||
exp: NullableTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegation signature payload structure
|
||||
*/
|
||||
export interface DelegationSigPayload {
|
||||
/** Varsig header */
|
||||
h: VarsigHeader;
|
||||
/** Delegation payload with version tag */
|
||||
"ucan/dlg@1.0.0-rc.1": DelegationPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete UCAN Delegation envelope
|
||||
*/
|
||||
export type Delegation = [Signature, DelegationSigPayload];
|
||||
|
||||
// =============================================================================
|
||||
// INVOCATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Proof chain - ordered CIDs of delegations from Subject to Invoker
|
||||
*/
|
||||
export type ProofChain = CID[];
|
||||
|
||||
/**
|
||||
* UCAN Invocation Payload (ucan/inv@1.0.0-rc.1)
|
||||
*/
|
||||
export interface InvocationPayload {
|
||||
/** Issuer DID - the invoker */
|
||||
iss: DID;
|
||||
/** Subject DID being invoked */
|
||||
sub: DID;
|
||||
/** Optional audience DID if executor differs from subject */
|
||||
aud?: DID;
|
||||
/** Command to execute */
|
||||
cmd: Command;
|
||||
/** Command arguments */
|
||||
args: Arguments;
|
||||
/** Proof chain of delegations */
|
||||
prf: ProofChain;
|
||||
/** Optional metadata */
|
||||
meta?: Metadata;
|
||||
/** Optional nonce for non-idempotent invocations */
|
||||
nonce?: Bytes;
|
||||
/** Expiration timestamp */
|
||||
exp: NullableTimestamp;
|
||||
/** Optional issuance timestamp */
|
||||
iat?: Timestamp;
|
||||
/** Optional CID of Receipt that enqueued this task */
|
||||
cause?: CID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invocation signature payload structure
|
||||
*/
|
||||
export interface InvocationSigPayload {
|
||||
/** Varsig header */
|
||||
h: VarsigHeader;
|
||||
/** Invocation payload with version tag */
|
||||
"ucan/inv@1.0.0-rc.1": InvocationPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete UCAN Invocation envelope
|
||||
*/
|
||||
export type Invocation = [Signature, InvocationSigPayload];
|
||||
|
||||
// =============================================================================
|
||||
// TASK
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* UCAN Task - subset of Invocation fields uniquely determining work
|
||||
* Task ID is the CID of these fields
|
||||
*/
|
||||
export interface Task {
|
||||
/** Subject DID */
|
||||
sub: DID;
|
||||
/** Command to execute */
|
||||
cmd: Command;
|
||||
/** Command arguments */
|
||||
args: Arguments;
|
||||
/** Nonce for uniqueness */
|
||||
nonce: Bytes;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RECEIPT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Successful execution result
|
||||
*/
|
||||
export interface SuccessResult<T = unknown> {
|
||||
ok: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed execution result
|
||||
*/
|
||||
export interface ErrorResult<E = unknown> {
|
||||
err: E;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution outcome
|
||||
*/
|
||||
export type ExecutionResult<T = unknown, E = unknown> = SuccessResult<T> | ErrorResult<E>;
|
||||
|
||||
/**
|
||||
* UCAN Receipt Payload - execution result
|
||||
*/
|
||||
export interface ReceiptPayload<T = unknown, E = unknown> {
|
||||
/** Executor DID */
|
||||
iss: DID;
|
||||
/** CID of executed Invocation */
|
||||
ran: CID;
|
||||
/** Execution result */
|
||||
out: ExecutionResult<T, E>;
|
||||
/** Effects - CIDs of Tasks to enqueue */
|
||||
fx?: CID[];
|
||||
/** Optional metadata */
|
||||
meta?: Metadata;
|
||||
/** Issuance timestamp */
|
||||
iat?: Timestamp;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REVOCATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Revocation arguments
|
||||
*/
|
||||
export interface RevocationArgs {
|
||||
/** CID of delegation to revoke */
|
||||
ucan: CID;
|
||||
}
|
||||
|
||||
/**
|
||||
* UCAN Revocation Payload
|
||||
*/
|
||||
export interface RevocationPayload {
|
||||
/** Revoker DID - must be issuer in delegation chain */
|
||||
iss: DID;
|
||||
/** Subject of delegation being revoked */
|
||||
sub: DID;
|
||||
/** Revocation command */
|
||||
cmd: "/ucan/revoke";
|
||||
/** Revocation arguments */
|
||||
args: RevocationArgs;
|
||||
/** Proof chain */
|
||||
prf: ProofChain;
|
||||
/** Nonce */
|
||||
nonce: Bytes;
|
||||
/** Expiration */
|
||||
exp: NullableTimestamp;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENVELOPE
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generic UCAN Envelope format
|
||||
*/
|
||||
export type UCANEnvelope<P extends object> = [
|
||||
Signature,
|
||||
{
|
||||
h: VarsigHeader;
|
||||
} & P
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// CRYPTOGRAPHY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Supported signature algorithms
|
||||
*/
|
||||
export type CryptoAlgorithm = "Ed25519" | "P-256" | "secp256k1";
|
||||
|
||||
/**
|
||||
* Supported hash algorithms
|
||||
*/
|
||||
export type HashAlgorithm = "sha2-256";
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Validation error codes
|
||||
*/
|
||||
export type ValidationErrorCode =
|
||||
| "EXPIRED"
|
||||
| "NOT_YET_VALID"
|
||||
| "INVALID_SIGNATURE"
|
||||
| "PRINCIPAL_MISALIGNMENT"
|
||||
| "POLICY_VIOLATION"
|
||||
| "REVOKED"
|
||||
| "INVALID_PROOF_CHAIN"
|
||||
| "UNKNOWN_COMMAND"
|
||||
| "MALFORMED_TOKEN";
|
||||
|
||||
/**
|
||||
* Validation error structure
|
||||
*/
|
||||
export interface ValidationError {
|
||||
code: ValidationErrorCode;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Successful validation result
|
||||
*/
|
||||
export interface ValidationSuccess {
|
||||
valid: true;
|
||||
capability: Capability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed validation result
|
||||
*/
|
||||
export interface ValidationFailure {
|
||||
valid: false;
|
||||
error: ValidationError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation result union
|
||||
*/
|
||||
export type ValidationResult = ValidationSuccess | ValidationFailure;
|
||||
|
||||
// =============================================================================
|
||||
// ROLES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* UCAN Agent Roles
|
||||
*/
|
||||
export interface Roles {
|
||||
/** General class of entities interacting with UCAN */
|
||||
Agent: DID;
|
||||
/** Principal delegated to in current UCAN (aud field) */
|
||||
Audience: DID;
|
||||
/** Agent that performs invocation action */
|
||||
Executor: DID;
|
||||
/** Principal requesting execution */
|
||||
Invoker: DID;
|
||||
/** Principal of current UCAN (iss field) */
|
||||
Issuer: DID;
|
||||
/** Subject controlling external resource */
|
||||
Owner: DID;
|
||||
/** Agent identified by DID */
|
||||
Principal: DID;
|
||||
/** Issuer in proof chain that revokes */
|
||||
Revoker: DID;
|
||||
/** Principal whose authority is delegated */
|
||||
Subject: DID;
|
||||
/** Agent interpreting UCAN validity */
|
||||
Validator: DID;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMON COMMANDS (for convenience)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Standard CRUD command arguments
|
||||
*/
|
||||
export namespace CRUDCommands {
|
||||
export interface CreateArgs {
|
||||
uri: string;
|
||||
payload: unknown;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ReadArgs {
|
||||
uri: string;
|
||||
query?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface UpdateArgs {
|
||||
uri: string;
|
||||
payload: unknown;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DeleteArgs {
|
||||
uri: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard messaging command arguments
|
||||
*/
|
||||
export namespace MessageCommands {
|
||||
export interface SendArgs {
|
||||
from: string;
|
||||
to: string[];
|
||||
subject?: string;
|
||||
body: string;
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
}
|
||||
|
||||
export interface ReceiveArgs {
|
||||
mailbox: string;
|
||||
since?: Timestamp;
|
||||
limit?: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebAssembly execution command arguments
|
||||
*/
|
||||
export namespace WasmCommands {
|
||||
export interface RunArgs {
|
||||
/** Wasm module as data URI or CID */
|
||||
mod: string | CID;
|
||||
/** Function to execute */
|
||||
fun: string;
|
||||
/** Function parameters */
|
||||
params: unknown[];
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TYPE GUARDS
|
||||
// =============================================================================
|
||||
|
||||
export function isDID(value: unknown): value is DID {
|
||||
return typeof value === "string" && value.startsWith("did:");
|
||||
}
|
||||
|
||||
export function isCID(value: unknown): value is CID {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"/" in value &&
|
||||
typeof (value as CID)["/"] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isBytes(value: unknown): value is Bytes {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"/" in value &&
|
||||
typeof (value as Bytes)["/"] === "object" &&
|
||||
"bytes" in (value as Bytes)["/"]
|
||||
);
|
||||
}
|
||||
|
||||
export function isSuccessResult<T>(result: ExecutionResult<T>): result is SuccessResult<T> {
|
||||
return "ok" in result;
|
||||
}
|
||||
|
||||
export function isErrorResult<E>(result: ExecutionResult<unknown, E>): result is ErrorResult<E> {
|
||||
return "err" in result;
|
||||
}
|
||||
|
||||
export function isDelegation(token: Delegation | Invocation): token is Delegation {
|
||||
return "ucan/dlg@1.0.0-rc.1" in token[1];
|
||||
}
|
||||
|
||||
export function isInvocation(token: Delegation | Invocation): token is Invocation {
|
||||
return "ucan/inv@1.0.0-rc.1" in token[1];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Extract the payload type from a UCAN envelope
|
||||
*/
|
||||
export type ExtractPayload<E extends UCANEnvelope<object>> =
|
||||
E extends UCANEnvelope<infer P> ? P : never;
|
||||
|
||||
/**
|
||||
* Create a typed invocation for a specific command
|
||||
*/
|
||||
export type TypedInvocation<C extends Command, A extends Arguments> = [
|
||||
Signature,
|
||||
{
|
||||
h: VarsigHeader;
|
||||
"ucan/inv@1.0.0-rc.1": Omit<InvocationPayload, "cmd" | "args"> & {
|
||||
cmd: C;
|
||||
args: A;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a typed delegation for a specific command
|
||||
*/
|
||||
export type TypedDelegation<C extends Command> = [
|
||||
Signature,
|
||||
{
|
||||
h: VarsigHeader;
|
||||
"ucan/dlg@1.0.0-rc.1": Omit<DelegationPayload, "cmd"> & {
|
||||
cmd: C;
|
||||
};
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user