Feat/1285 es ucan formatting (#1302)

* feat: Add Enclave Usage Examples

* feat(es/ucan): Add comprehensive integration tests

- Create integration.test.ts with full UCAN token lifecycle testing
- Cover end-to-end token creation, parsing, and validation
- Test capability attenuation and delegation chains
- Validate multi-algorithm support and timestamp scenarios
- Implement error recovery and performance test scenarios

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* No commit suggestions generated

* chore: Remove migrated components and add migration documentation

Removed all code and references for components that have been moved to separate repositories:

**Moved to sonr-io/hway:**
- bridge/ - HTTP service with OAuth2/OIDC/WebAuthn handlers
- cmd/hway/ - Highway service binary
- internal/migrations/ - PostgreSQL schema migrations

**Moved to sonr-io/motr:**
- cmd/motr/ - Motor worker service (WASM vault operations)
- cmd/vault/ - Vault CLI tool
- crypto/ - Comprehensive cryptographic library
- packages/ - TypeScript SDK packages (es, sdk, ui, com, pkl)
- web/auth/ - Authentication web application
- web/dash/ - Dashboard web application

**Updated Configuration:**
- Makefile: Removed build/test/release targets for moved components
- CLAUDE.md: Simplified to focus on core blockchain components
- devbox.json: Removed scripts for moved services
- docker-compose.yml: Removed hway, postgres, redis, auth, dash services
- .github/scopes.yml: Removed CI scopes for migrated components
- .goreleaser.yml: Updated release configuration

**Added Migration Documentation:**
- MIGRATE_HWAY.md: Comprehensive Highway service architecture and migration guide
- MIGRATE_MOTR.md: Comprehensive Motor/Worker/Vault architecture and migration guide

These migration documents provide complete context for setting up the new repositories including architecture diagrams, component breakdowns, API documentation, and migration checklists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* chore: Remove contracts references and documentation

Removed all references to the contracts directory that was migrated to a separate repository.

**Changes:**
- .gitignore: Removed contract-specific ignore patterns for DAO and wSNR contracts
- .gitignore: Removed hway and motr binary references (already migrated)
- .rgignore: Removed contracts, chains, and crypto directory references
- docs/reference/contracts/: Removed DAO.mdx and wSNR.mdx documentation files

This completes the cleanup of migrated components from the repository.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add crypto library migration documentation

Added comprehensive migration documentation for the crypto library that was
moved to sonr-io/crypto repository. This documentation provides complete context
for understanding the cryptographic primitives and protocols used throughout
the Sonr ecosystem.

## Key Documentation Added

### MIGRATE_CRYPTO.md
Complete documentation of the crypto library covering:

**Core Cryptographic Primitives**
- Elliptic curve implementations (Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta)
- Native curve arithmetic with optimized field operations
- Pairing-friendly curves for BLS signatures

**Multi-Party Computation (MPC)**
- MPC enclave for vault key generation and management
- Threshold cryptography (TECDSA, TED25519 with FROST protocol)
- Distributed Key Generation (DKG) via Gennaro and FROST protocols
- Secret sharing schemes (Shamir, Feldman VSS, Pedersen VSS)

**Digital Signature Schemes**
- BLS signatures with aggregation support
- BBS+ signatures for selective disclosure
- Schnorr signatures (standard and Mina/NEM variants)
- ECDSA with deterministic nonce generation

**Zero-Knowledge Proofs**
- Bulletproofs for range proofs
- Inner Product Arguments (IPA)
- Batch verification support

**Advanced Cryptographic Protocols**
- Cryptographic accumulators for set membership proofs
- Paillier homomorphic encryption
- Oblivious Transfer (OT) protocols
- Verifiable Random Functions (VRF)

**Key Management & Identity**
- DID key management with multi-chain support
- Multi-algorithm public key handling
- Wallet address derivation (Bitcoin, Ethereum, Cosmos, Solana, etc.)

**UCAN Integration**
- User-Controlled Authorization Networks
- Capability delegation and attenuation
- JWT-based capability tokens
- MPC-enabled UCAN signing

**Security Utilities**
- AEAD encryption (AES-GCM, AES-SIV)
- Argon2 key derivation
- ECIES encryption
- Secure memory handling

### MIGRATE_MOTR.md Updates
Updated Motor migration documentation to clarify that the crypto library
is now a separate external dependency at github.com/sonr-io/crypto v1.0.1

## Repository Context

The crypto library has been successfully migrated to its own repository
and is published as a Go module. It serves as the foundational cryptographic
layer for:
- Sonr blockchain (snrd) - DID signatures, vault operations
- Highway service (hway) - UCAN token signing, WebAuthn
- Motor/Worker (motr) - MPC vault operations, threshold signatures

## Integration Impact

All Sonr ecosystem components now depend on the external crypto library:
```go
require github.com/sonr-io/crypto v1.0.1
```

The migration enables independent versioning and maintenance of cryptographic
primitives while maintaining security and compatibility across the ecosystem.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* No commit suggestions generated

* No commit suggestions generated

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Prad Nukala
2025-10-10 11:47:18 -04:00
committed by GitHub
co-authored by Claude
parent 7570d67787
commit 40eadc995e
1306 changed files with 2854 additions and 316038 deletions
View File
+3 -38
View File
@@ -7,9 +7,6 @@
- app/**
- go.mod
- go.sum
- name: crypto
include:
- crypto/**
- name: devops
include:
- .github/**
@@ -22,52 +19,20 @@
- name: docs
include:
- docs/**
- name: hway
include:
- cmd/hway/**
- bridge/**
- types/**
- name: dex
include:
- proto/dex/**
- x/dex/**
- name: did
include:
- proto/dex/**
- proto/did/**
- x/did/**
- name: dwn
include:
- proto/dex/**
- proto/dwn/**
- x/dwn/**
- name: svc
include:
- proto/dex/**
- proto/svc/**
- x/svc/**
- name: motr
include:
- cmd/motr/**
- name: vault
include:
- cmd/vault/**
- name: com
include:
- packages/com/**
- name: es
include:
- packages/es/**
- name: pkl
include:
- packages/pkl/**
- name: sdk
include:
- packages/sdk/**
- name: ui
include:
- packages/ui/**
- name: auth
include:
- web/auth/**
- name: dash
include:
- web/dash/**
-19
View File
@@ -31,13 +31,7 @@ out/
# Go binaries (but allow docs references)
snrd
hway
motr
!cmd/motr/
!cmd/hway/
!cmd/snrd/
!docs/**/motr
!docs/**/hway
# Allow specific CLI binaries
!cli/join-testnet/bin/
@@ -73,21 +67,8 @@ artifacts/
**/target/
**/Cargo.lock
# Contract-specific
contracts/DAO/deployment_ids.env
contracts/DAO/deployment_report_*.md
contracts/DAO/relayer.pid
contracts/DAO/*.mnemonic
contracts/DAO/tx_*.json
contracts/DAO/signed_*.json
contracts/DAO/init_*.json
contracts/DAO/MAINNET_DEPLOYMENT_INSTRUCTIONS.md
contracts/DAO/backups/
contracts/wSNR/remappings.txt
# ===== Blockchain & Cosmos SDK =====
.snrd/
.hway/
keyring-test/
mytestnet/
docker/testnet/testnet-data
+326 -2
View File
@@ -1,5 +1,329 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
---
version: 2
monorepo:
tag_prefix: v
dist: dist/snrd
project_name: snrd
before:
hooks:
- go mod download
builds:
# Darwin AMD64 Build
- id: snrd-darwin-amd64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
- CGO_LDFLAGS=-lm
goos:
- darwin
goarch:
- amd64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Darwin ARM64 Build
- id: snrd-darwin-arm64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=oa64-clang
- CXX=oa64-clang++
- CGO_LDFLAGS=-lm
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Linux AMD64 Build
- id: snrd-linux-amd64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++
- CGO_LDFLAGS=-lm
goos:
- linux
goarch:
- amd64
goamd64:
- v1
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Linux ARM64 Build
- id: snrd-linux-arm64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
- CGO_LDFLAGS=-lm
goos:
- linux
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
aur_sources:
- name: snrd
disable: true
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity and data storage network"
maintainers:
- "Sonr <support@sonr.io>"
license: "GPL-3.0"
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://[email protected]/snrd.git"
skip_upload: auto
provides:
- snrd
conflicts:
- snrd-bin
depends:
- glibc
makedepends:
- go
- git
- make
commit_msg_template: "Update to {{ .Tag }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
prepare: |-
cd "${pkgname}_${pkgver}"
go mod download
build: |-
cd "${pkgname}_${pkgver}"
export CGO_ENABLED=1
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw"
go build \
-ldflags="-w -s -buildid='' -linkmode=external \
-X github.com/cosmos/cosmos-sdk/version.Name=sonr \
-X github.com/cosmos/cosmos-sdk/version.AppName=snrd \
-X github.com/cosmos/cosmos-sdk/version.Version=${pkgver} \
-X 'github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger'" \
-tags "netgo,ledger" \
-o snrd ./cmd/snrd
chmod +x ./snrd
package: |-
cd "${pkgname}_${pkgver}"
# bin
install -Dm755 "./snrd" "${pkgdir}/usr/bin/snrd"
# license
if [ -f "./LICENSE" ]; then
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/snrd/LICENSE"
fi
# readme
if [ -f "./README.md" ]; then
install -Dm644 "./README.md" "${pkgdir}/usr/share/doc/snrd/README.md"
fi
# config directory
install -dm755 "${pkgdir}/etc/snrd"
install -dm755 "${pkgdir}/var/lib/snrd"
backup:
- /etc/snrd/config.toml
- /etc/snrd/app.toml
nix:
- name: snrd
ids:
- snrd
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity network"
license: "gpl3"
path: pkgs/snrd/default.nix
commit_msg_template: "snrd: {{ .Tag }}"
dependencies:
- stdenv
- glibc
extra_install: |-
wrapProgram $out/bin/snrd --prefix PATH : ${lib.makeBinPath [ glibc stdenv.cc.cc.lib ]}
repository:
owner: sonr-io
name: nur
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
archives:
- id: snrd
ids:
- snrd-linux-amd64
- snrd-linux-arm64
- snrd-darwin-amd64
- snrd-darwin-arm64
name_template: >-
snrd_{{ .Os }}_{{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }}
formats: ["tar.gz"]
files:
- src: README*
wrap_in_directory: false
homebrew_casks:
- name: snrd
ids:
- snrd
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity and data storage network"
commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"
directory: Casks
repository:
owner: sonr-io
name: homebrew-tap
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/snrd"]
end
nfpms:
- id: snrd
package_name: snrd
ids:
- snrd-linux-amd64
- snrd-linux-arm64
file_name_template: "snrd_{{ .Os }}_{{ .Arch }}{{ .ConventionalExtension }}"
vendor: Sonr
homepage: "https://sonr.io"
maintainer: "Sonr <support@sonr.io>"
description: "Sonr is a decentralized, permissionless, and censorship-resistant identity network."
license: "GPL-3.0"
formats:
- rpm
- deb
- apk
- archlinux
contents:
- src: README*
dst: /usr/share/doc/snrd
bindir: /usr/bin
section: net
priority: optional
blobs:
- provider: s3
endpoint: https://eb37925850388bca807b7fab964c12bb.r2.cloudflarestorage.com
bucket: releases
region: auto
directory: "snrd/{{ .Tag }}"
ids:
- snrd
release:
disable: false
github:
owner: sonr-io
name: sonr
name_template: "{{ .Tag }}"
draft: false
replace_existing_draft: false # Don't replace drafts
replace_existing_artifacts: false # Append, don't replace
mode: append # Explicitly set to append mode
checksum:
name_template: "snrd_checksums.txt"
npms:
- name: "@sonr.io/snrd"
ids:
- snrd
description: "Sonr blockchain daemon - decentralized identity and data storage network"
homepage: "https://sonr.io"
license: "GPL-3.0"
author: "Sonr <support@sonr.io>"
repository: "https://github.com/sonr-io/sonr"
bugs: "https://github.com/sonr-io/sonr/issues"
keywords:
- blockchain
- cosmos
- did
- identity
- web3
- sonr
access: public
format: tar.gz
snapshot:
version_template: "{{ .Branch }}-{{ .ShortCommit }}-dev"
# Changelog configuration
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
-3
View File
@@ -1,9 +1,6 @@
.dockerignore
.goreleaser.yml
api
contracts
chains
crypto
proto
test
**/_test.go
+847
View File
@@ -0,0 +1,847 @@
# Sonr Cryptography Library Migration Context
> **Repository Migration**: `sonr-io/sonr/crypto/` → `sonr-io/crypto`
> **Package Name**: `github.com/sonr-io/crypto`
> **Current Version**: `v1.0.1`
## Overview
The Sonr Cryptography Library is a comprehensive collection of cryptographic primitives designed for secure decentralized applications. It provides enterprise-grade implementations of elliptic curve cryptography, multi-party computation, threshold cryptography, zero-knowledge proofs, and advanced signature schemes.
**Key Features:**
- **Multi-Party Computation (MPC)**: Secure distributed key generation and signing
- **Threshold Cryptography**: TECDSA and TED25519 with FROST protocol
- **Advanced Signatures**: BLS aggregation, BBS+ selective disclosure, Schnorr variants
- **Secret Sharing**: Shamir, Feldman VSS, Pedersen VSS implementations
- **Zero-Knowledge Proofs**: Bulletproofs for range proofs
- **Multiple Elliptic Curves**: Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta
- **UCAN Integration**: Capability-based authorization tokens
- **DID Key Management**: Multi-chain wallet address derivation
## Repository Structure
```
sonr-io/crypto/
├── core/ # Core cryptographic primitives
│ ├── curves/ # Elliptic curve implementations
│ │ ├── native/ # Native curve arithmetic
│ │ │ ├── bls12381/ # BLS12-381 pairing-friendly curve
│ │ │ ├── k256/ # Secp256k1 (Bitcoin/Ethereum)
│ │ │ ├── p256/ # NIST P-256
│ │ │ └── pasta/ # Pallas/Vesta for ZK proofs
│ │ ├── bls12377_curve.go
│ │ ├── bls12381_curve.go
│ │ ├── ed25519_curve.go
│ │ ├── k256_curve.go
│ │ ├── p256_curve.go
│ │ └── pallas_curve.go
│ ├── protocol/ # MPC protocol framework
│ ├── commit.go # Pedersen commitments
│ ├── hash.go # Cryptographic hash utilities
│ └── mod.go # Modular arithmetic
├── mpc/ # Multi-Party Computation
│ ├── enclave.go # MPC enclave management
│ ├── protocol.go # DKG and signing protocols
│ ├── codec.go # Serialization/deserialization
│ ├── import.go # Enclave import/export
│ ├── verify.go # Signature verification
│ └── spec/ # UCAN/JWT specifications
├── tecdsa/ # Threshold ECDSA
│ └── dklsv1/ # 2-party ECDSA (DKLS v1)
│ ├── dkg/ # Distributed key generation
│ ├── sign/ # Threshold signing
│ ├── refresh/ # Key refresh protocol
│ └── dealer/ # Trusted dealer mode
├── ted25519/ # Threshold Ed25519
│ ├── frost/ # FROST protocol (DKG + signing)
│ └── ted25519/ # Core threshold Ed25519
├── signatures/ # Digital signature schemes
│ ├── bls/ # BLS signatures
│ │ └── bls_sig/ # Aggregatable BLS
│ ├── bbs/ # BBS+ selective disclosure
│ ├── schnorr/ # Schnorr variants
│ │ ├── mina/ # Mina protocol integration
│ │ └── nem/ # NEM blockchain support
│ └── common/ # Shared signature utilities
├── sharing/ # Secret sharing schemes
│ ├── shamir.go # Shamir's Secret Sharing
│ ├── feldman.go # Feldman VSS
│ ├── pedersen.go # Pedersen VSS
│ └── v1/ # Version 1 implementations
├── dkg/ # Distributed Key Generation
│ ├── frost/ # FROST DKG for Ed25519
│ ├── gennaro/ # Gennaro DKG protocol
│ └── gennaro2p/ # 2-party simplified DKG
├── bulletproof/ # Bulletproofs (range proofs)
│ ├── range_prover.go # Range proof generation
│ ├── range_verifier.go # Range proof verification
│ ├── ipp_prover.go # Inner product argument
│ └── generators.go # Generator points
├── accumulator/ # Cryptographic accumulators
│ ├── accumulator.go # RSA accumulator
│ ├── witness.go # Membership witnesses
│ └── proof.go # Inclusion/exclusion proofs
├── paillier/ # Paillier homomorphic encryption
│ ├── paillier.go # Public/private key operations
│ └── psf.go # Proof of safe factorization
├── ot/ # Oblivious Transfer
│ ├── base/simplest/ # Simplest OT protocol
│ └── extension/kos/ # KOS OT extension
├── zkp/ # Zero-Knowledge Proofs
│ └── schnorr/ # Schnorr proofs of knowledge
├── ucan/ # User-Controlled Authorization Networks
│ ├── capability.go # Capability management
│ ├── crypto.go # UCAN cryptographic operations
│ ├── jwt.go # JWT-based UCAN tokens
│ ├── verifier.go # Delegation chain verification
│ └── vault.go # Vault-specific capabilities
├── keys/ # Key management utilities
│ ├── didkey.go # DID key format support
│ ├── pubkey.go # Public key operations
│ └── parsers/ # Multi-chain key parsers
│ ├── btc_parser.go # Bitcoin key parsing
│ ├── eth_parser.go # Ethereum key parsing
│ ├── cosmos_parser.go # Cosmos SDK parsing
│ ├── sol_parser.go # Solana key parsing
│ └── ... # Other blockchain parsers
├── aead/ # Authenticated encryption
│ └── aes_gcm.go # AES-GCM AEAD
├── daed/ # Deterministic AEAD
│ └── aes_siv.go # AES-SIV encryption
├── ecies/ # Elliptic Curve IES
│ ├── encrypt.go # ECIES encryption
│ └── keys.go # Key generation
├── argon2/ # Password hashing
│ └── kdf.go # Argon2 key derivation
├── vrf/ # Verifiable Random Functions
│ └── vrf.go # Curve25519 VRF
├── ecdsa/ # ECDSA utilities
│ ├── canonical.go # Canonical signature encoding
│ └── deterministic.go # RFC 6979 deterministic signing
├── subtle/ # Low-level crypto utilities
│ ├── hkdf.go # HKDF key derivation
│ ├── random/ # Secure randomness
│ └── x25519.go # X25519 key exchange
└── internal/ # Internal utilities
├── ed25519/ # Extended Ed25519 operations
├── hash.go # Hash utilities
└── point.go # Point operations
```
## Core Modules
### 1. Multi-Party Computation (`mpc/`)
**Purpose**: Secure distributed key generation and threshold signing without trusted dealers.
#### MPC Enclave 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
}
```
#### Key Functions
```go
// Generate new MPC enclave (2-of-2 threshold)
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)
// Verify signature with public key
func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error)
```
#### Security Features
- **2-of-2 Threshold**: Both parties required for signing
- **No Single Point of Failure**: Neither party can sign alone
- **Proactive Refresh**: Key rotation without changing public key
- **AES-GCM Encryption**: Secure enclave data encryption
- **SHA3-256 Hashing**: Cryptographic hash operations
#### Supported Curves
- `K256` - Secp256k1 (Bitcoin, Ethereum)
- `P256` - NIST P-256
- `ED25519` - Twisted Edwards curve
- `BLS12381` - Pairing-friendly curve
### 2. Elliptic Curves (`core/curves/`)
**Purpose**: Comprehensive elliptic curve implementations with unified interfaces.
#### Supported Curves
**Ed25519**
- Twisted Edwards curve for EdDSA signatures
- High-performance, constant-time operations
- Used in: Cosmos SDK, Solana, many modern systems
**Secp256k1 (K256)**
- Bitcoin and Ethereum standard curve
- ECDSA signature support
- Native field arithmetic implementations
**P-256 (Secp256r1)**
- NIST standard curve
- FIPS 186-4 compliant
- Wide hardware acceleration support
**BLS12-381**
- Pairing-friendly curve for BLS signatures
- Optimal ate pairing support
- Signature aggregation capabilities
- G1, G2, and GT group operations
**BLS12-377**
- Alternative pairing curve
- Used in certain ZK-SNARK constructions
**Pallas/Vesta**
- Pasta curves for recursive ZK proofs
- Cycle of curves for composition
#### Curve Interface
```go
type Curve interface {
Scalar
Point
Name() string
NewIdentityPoint() Point
NewGeneratorPoint() Point
Hash(input []byte) Point
// ... additional methods
}
```
### 3. Signature Schemes (`signatures/`)
#### BLS Signatures (`signatures/bls/`)
**Features:**
- Signature aggregation (combine multiple signatures)
- Threshold signatures (t-of-n)
- Multi-signatures with proof of possession
- Both G1 and G2 variants (tiny_bls and usual_bls)
**Key Operations:**
```go
// Sign message
func (sk *SecretKey) Sign(msg []byte) *Signature
// Aggregate multiple signatures
func AggregateSignatures(sigs ...*Signature) (*MultiSignature, error)
// Verify aggregated signature
func (sig *Signature) AggregateVerify(pks []*PublicKey, msgs [][]byte) (bool, error)
// Threshold key generation
func ThresholdGenerateKeys(threshold, total int) (*PublicKey, []*SecretKeyShare, error)
```
#### BBS+ Signatures (`signatures/bbs/`)
**Purpose**: Privacy-preserving signatures with selective disclosure
**Features:**
- Blind signatures for credential issuance
- Selective disclosure of attributes
- Zero-knowledge proofs of possession
- Unlinkable presentations
**Use Cases:**
- Verifiable credentials
- Anonymous authentication
- Privacy-preserving identity systems
#### Schnorr Signatures (`signatures/schnorr/`)
**Mina Protocol** (`mina/`):
- Poseidon hash function
- Schnorr signatures for Mina blockchain
- Challenge derivation
**NEM/Symbol** (`nem/`):
- Ed25519-Keccak variant
- NEM blockchain compatibility
### 4. Secret Sharing (`sharing/`)
#### Shamir's Secret Sharing
```go
type Shamir struct {
Threshold int
Limit int
Curve Curve
}
// Split secret into shares
func (s *Shamir) Split(secret []byte) ([]*ShamirShare, error)
// Reconstruct secret from shares
func (s *Shamir) Combine(shares []*ShamirShare) ([]byte, error)
```
#### Feldman Verifiable Secret Sharing
**Added Security**: Public commitments for share verification
```go
type FeldmanVerifier struct {
Commitments []curves.Point
}
// Verify share validity
func (v *FeldmanVerifier) Verify(share *ShamirShare) error
```
#### Pedersen Verifiable Secret Sharing
**Enhanced Privacy**: Computationally binding commitments
```go
// Split with verifiable commitments
func (p *Pedersen) Split(secret []byte) (*PedersenResult, error)
```
### 5. Threshold Cryptography
#### TECDSA (`tecdsa/dklsv1/`)
**Protocol**: Two-party threshold ECDSA (DKLS v1)
**Components:**
- **DKG**: Distributed key generation without trusted dealer
- **Signing**: Threshold signature generation
- **Refresh**: Proactive keyshare rotation
- **Dealer**: Optional trusted dealer mode
**Key Features:**
- No trusted third party required
- Active security against malicious adversaries
- Compatible with standard ECDSA verification
#### TED25519 (`ted25519/`)
**FROST Protocol** (`frost/`):
- Flexible Round-Optimized Schnorr Threshold signatures
- Efficient threshold Ed25519 signatures
- Three-round signing protocol
**Core Operations** (`ted25519/`):
```go
// Threshold key generation
func KeyGen(threshold, total int) ([]*SecretKeyShare, *PublicKey, error)
// Partial signature generation
func ThresholdSign(expandedSecretKeyShare []byte, publicKey []byte,
rShare []byte, R []byte, message []byte) []byte
// Signature aggregation
func AggregateSignatures(partialSigs [][]byte, R []byte) ([]byte, error)
```
### 6. Distributed Key Generation (`dkg/`)
#### Gennaro DKG (`dkg/gennaro/`)
**Standard DKG protocol** with:
- Four-round protocol
- Pedersen commitments
- Complaint handling
- Byzantine fault tolerance
#### FROST DKG (`dkg/frost/`)
**Optimized for Ed25519**:
- Two-round DKG
- Simplified complaint phase
- Integration with FROST signing
#### 2-Party DKG (`dkg/gennaro2p/`)
**Simplified protocol** for two parties:
- Reduced communication overhead
- Faster execution
- Suitable for client-server architectures
### 7. Zero-Knowledge Proofs
#### Bulletproofs (`bulletproof/`)
**Range Proofs without Trusted Setup**:
```go
// Prove value in range [0, 2^n]
func (p *RangeProver) Prove(v *big.Int, n int) (*RangeProof, error)
// Verify range proof
func (v *RangeVerifier) Verify(proof *RangeProof, commitment Point, n int) (bool, error)
// Batched range proofs (aggregate multiple proofs)
func BatchProve(values []*big.Int, n int) (*RangeProof, error)
```
**Features:**
- Logarithmic proof size: O(log n)
- Inner product arguments
- Batch verification support
- No trusted setup required
**Applications:**
- Confidential transactions
- Private smart contracts
- Privacy-preserving audits
#### Schnorr Proofs (`zkp/schnorr/`)
**Proof of Knowledge**:
- Discrete logarithm proofs
- Commitment proofs
- Non-interactive via Fiat-Shamir
### 8. Advanced Cryptography
#### Cryptographic Accumulators (`accumulator/`)
**RSA Accumulator** for set membership:
```go
// Add element to accumulator
func (acc *Accumulator) Add(element []byte) (*Witness, error)
// Generate membership proof
func (w *Witness) GenerateProof() (*Proof, error)
// Verify membership
func (acc *Accumulator) Verify(element []byte, proof *Proof) bool
```
**Use Cases:**
- Revocation lists
- Anonymous credentials
- Blockchain state commitments
#### Paillier Encryption (`paillier/`)
**Homomorphic Properties**:
- Additive homomorphism: E(m1) * E(m2) = E(m1 + m2)
- Scalar multiplication: E(m)^k = E(k * m)
- Threshold decryption support
**Applications:**
- Private computation
- Secure multi-party computation
- E-voting systems
#### Oblivious Transfer (`ot/`)
**Simplest OT** (`base/simplest/`):
- 1-out-of-2 OT protocol
- Based on Curve25519
**KOS Extension** (`extension/kos/`):
- Extend base OT to many OTs
- Efficient batch operations
- Correlated randomness generation
**Applications:**
- Private set intersection
- Secure two-party computation
- Password-authenticated key exchange
### 9. UCAN (User-Controlled Authorization Networks) (`ucan/`)
**Capability-Based Authorization**:
```go
// Create UCAN token
func CreateUCAN(issuer DID, audience DID, capabilities []Capability) (string, error)
// Attenuate capabilities (reduce permissions)
func AttenuateUCAN(parentToken string, newCapabilities []Capability) (string, error)
// Verify delegation chain
func VerifyDelegationChain(tokenString string, rootDID string) error
```
**Capability Types**:
- DID capabilities (read, write, update)
- DWN capabilities (records, protocols)
- Vault capabilities (sign, decrypt)
- DEX capabilities (swap, provide liquidity)
**Features:**
- JWT-based tokens
- Delegation chains
- Capability attenuation
- Proof-of-possession
- Expiration and not-before timestamps
### 10. Key Management (`keys/`)
#### DID Key Support (`didkey.go`)
```go
// Create DID from public key
func NewDID(publicKey []byte, keyType crypto.KeyType) (*DID, error)
// Derive blockchain address from DID
func (did *DID) Address() (string, error)
// Get raw public key bytes
func (did *DID) Raw() ([]byte, error)
```
#### Multi-Chain Parsers (`parsers/`)
**Supported Blockchains**:
- Bitcoin (BTC) - BIP32/BIP44 derivation
- Ethereum (ETH) - Keccak addresses
- Cosmos SDK - Bech32 encoding
- Solana (SOL) - Ed25519 keys
- Filecoin (FIL) - Secp256k1 keys
- TON - Ed25519 keys
### 11. Encryption Utilities
#### AEAD (`aead/`)
**AES-GCM Authenticated Encryption**:
```go
const (
KeySize = 32 // 256-bit key
NonceSize = 12 // 96-bit nonce
TagSize = 16 // 128-bit auth tag
)
// Encrypt with automatic nonce generation
func (c *AESGCMCipher) Encrypt(plaintext, aad []byte) ([]byte, error)
// Decrypt and verify
func (c *AESGCMCipher) Decrypt(ciphertext, aad []byte) ([]byte, error)
```
#### DAED (`daed/`)
**Deterministic AES-SIV**:
- Same plaintext → same ciphertext
- Useful for encrypted indices
- Misuse-resistant
#### ECIES (`ecies/`)
**Elliptic Curve Integrated Encryption Scheme**:
```go
// Generate ECIES keypair
func GenerateKey(curve Curve) (*PrivateKey, error)
// Encrypt message to public key
func Encrypt(recipientPubKey *PublicKey, message []byte) ([]byte, error)
// Decrypt with private key
func (sk *PrivateKey) Decrypt(ciphertext []byte) ([]byte, error)
```
### 12. Utility Modules
#### VRF (`vrf/`)
**Verifiable Random Function (Curve25519)**:
```go
// Generate VRF output and proof
func (sk *PrivateKey) Prove(message []byte) (vrf []byte, proof []byte)
// Verify VRF proof
func (pk *PublicKey) Verify(message, vrf, proof []byte) bool
```
**Applications:**
- Leader election
- Lottery systems
- Randomness beacons
- Sortition algorithms
#### Argon2 (`argon2/`)
**Password-Based Key Derivation**:
```go
// Derive key from password
func DeriveKey(password, salt []byte, keyLen uint32) []byte
```
**Parameters**:
- Time cost: 1 iteration (configurable)
- Memory cost: 64 MB (configurable)
- Parallelism: 4 threads (configurable)
#### ECDSA Utilities (`ecdsa/`)
**Canonical Encoding**:
- BIP 66 / RFC 6979 compliance
- Deterministic signature generation
- Low-S normalization
## Integration Patterns
### Usage in Sonr Blockchain
The crypto library is heavily integrated throughout the Sonr ecosystem:
#### DID Module
```go
import "github.com/sonr-io/crypto/keys"
import "github.com/sonr-io/crypto/mpc"
// DID creation from MPC enclave
enclave, _ := mpc.NewEnclave()
pubKey := enclave.GetPubPoint()
did := keys.NewDID(pubKey.Bytes(), crypto.Secp256k1)
```
#### DWN Module
```go
import "github.com/sonr-io/crypto/mpc"
import "github.com/sonr-io/crypto/aead"
// Vault operations
enclave := keeper.LoadEnclave(ctx, vaultID)
signature, _ := enclave.Sign(message)
// Encrypted data storage
cipher := aead.NewAESGCMCipher(key)
encrypted, _ := cipher.Encrypt(data, nil)
```
#### Service Module
```go
import "github.com/sonr-io/crypto/ucan"
// UCAN capability verification
verifier := ucan.NewVerifier(didResolver)
err := verifier.VerifyDelegationChain(ctx, tokenString)
```
### Motor Worker Integration
The WASM worker uses the crypto library extensively:
```go
import (
"github.com/sonr-io/crypto/mpc"
"github.com/sonr-io/crypto/core/curves"
)
//go:wasmexport sign
func sign() int32 {
// Load enclave from WASM memory
enclave := loadEnclave()
// Sign message
signature, _ := enclave.Sign(message)
return writeOutput(signature)
}
```
## Security Considerations
### Threat Model
The library is designed to protect against:
**Key Compromise**:
- MPC threshold schemes prevent single points of failure
- Proactive refresh rotates keyshares
**Insider Threats**:
- Multi-party protocols require cooperation
- No single party can perform operations alone
**Network Attacks**:
- Protocol messages are cryptographically protected
- Authentication prevents man-in-the-middle attacks
**Side-Channel Attacks**:
- Constant-time implementations where critical
- Secure memory handling
- Zeroization of sensitive data
### Best Practices
1. **Key Management**:
- Use hardware security modules when available
- Implement secure key backup and recovery
- Regular keyshare rotation via refresh protocols
2. **MPC Operations**:
- Secure communication channels (TLS)
- Proper authentication of parties
- Audit logging of all operations
3. **Random Number Generation**:
- Use `crypto/rand` for all random values
- Never reuse nonces in AEAD
- Verify randomness quality in production
4. **Error Handling**:
- Don't leak sensitive information in errors
- Validate all inputs
- Use constant-time comparisons for secrets
## Testing
The library includes comprehensive tests:
```bash
# Run all tests
go test ./...
# Run with race detection
go test -race ./...
# Generate coverage report
go test -cover ./...
# Run specific module tests
go test ./mpc/...
go test ./signatures/bls/...
go test ./bulletproof/...
# Benchmark performance
go test -bench=. ./core/curves/...
```
### Test Coverage
- **MPC**: Enclave operations, protocol execution, refresh
- **Signatures**: BLS aggregation, BBS+ proofs, Schnorr
- **Secret Sharing**: Shamir, Feldman, Pedersen
- **Threshold Crypto**: TECDSA, TED25519, DKG protocols
- **ZK Proofs**: Bulletproofs range proofs, Schnorr proofs
- **Encryption**: AEAD, ECIES, Paillier
- **Curves**: All curve operations, point arithmetic
## Dependencies
### External Libraries
```go
require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // Bitcoin crypto
github.com/consensys/gnark-crypto v0.19.0 // BLS12-377/381
golang.org/x/crypto v0.42.0 // Standard crypto
github.com/golang-jwt/jwt/v5 v5.3.0 // JWT tokens
)
```
### Internal Dependencies
The crypto library is **self-contained** and has no dependencies on other Sonr modules, making it suitable for independent use.
## Performance Characteristics
### Benchmarks (on AMD64, 2.5 GHz)
**Elliptic Curve Operations**:
- K256 scalar multiplication: ~50 µs
- Ed25519 signing: ~25 µs
- BLS12-381 pairing: ~1.2 ms
**MPC Operations**:
- DKG (2-party): ~15 ms
- Threshold signing: ~10 ms
- Key refresh: ~12 ms
**Signature Schemes**:
- BLS aggregation (100 sigs): ~150 ms
- BBS+ proof generation: ~80 ms
- Schnorr signing: ~30 µs
**Zero-Knowledge Proofs**:
- Bulletproof (64-bit range): ~40 ms
- Verification: ~25 ms
## Migration Checklist
When using the crypto library in a new project:
- [ ] Add dependency: `go get github.com/sonr-io/crypto@v1.0.1`
- [ ] Import required modules
- [ ] Initialize curve instances as needed
- [ ] Set up secure random number generation
- [ ] Implement proper error handling
- [ ] Add comprehensive tests
- [ ] Review security best practices
- [ ] Benchmark critical operations
- [ ] Set up monitoring/logging
- [ ] Document cryptographic assumptions
## Version Compatibility
**Go Version**: 1.24.4+
**Cosmos SDK**: Compatible with v0.50.x (if using Cosmos integration)
**Semantic Versioning**: The library follows semver
- Major: Breaking API changes
- Minor: New features, backwards compatible
- Patch: Bug fixes
## Related Documentation
- [Cosmos SDK Cryptography](https://docs.cosmos.network/main/learn/advanced/crypto)
- [BLS Signatures Spec](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature)
- [FROST Paper](https://eprint.iacr.org/2020/852)
- [Bulletproofs Paper](https://eprint.iacr.org/2017/1066)
- [UCAN Spec](https://github.com/ucan-wg/spec)
- [W3C DID Core](https://www.w3.org/TR/did-core/)
## Support
**Repository**: https://github.com/sonr-io/crypto
**Issues**: https://github.com/sonr-io/crypto/issues
**License**: Apache 2.0
+435
View File
@@ -0,0 +1,435 @@
# Highway (hway) Migration Context
> **Repository Migration**: `sonr-io/sonr` → `sonr-io/hway`
> **Components Moved**: `cmd/hway/`, `bridge/`, `internal/migrations/`
## Overview
Highway is a high-performance, PostgreSQL-backed HTTP service that handles OAuth2/OIDC authentication, WebAuthn flows, and asynchronous vault operations for the Sonr blockchain ecosystem. It serves as the authentication and task processing layer between clients and the Sonr blockchain.
## Architecture
### Core Technology Stack
- **Go**: 1.24.4
- **Task Queue**: Asynq (Redis-backed distributed task queue)
- **Actor System**: Proto.Actor for concurrency management
- **Database**: PostgreSQL with database/sql
- **Web Framework**: Echo v4
- **Authentication**: OAuth2, OIDC, WebAuthn, SIOP
- **Cryptography**: UCAN tokens, JWT signing (RS256)
### Service Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Highway Service (hway) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Bridge │ │ Tasks │ │ Handlers │ │
│ │ (HTTP) │──│ (Asynq) │──│ (Auth) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Proto.Actor System │ │
│ │ (Vault Actor Management) │ │
│ └─────────────────────────────────────────┘ │
│ │
└───────────┬───────────────────────┬─────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│PostgreSQL│ │ Redis │
│ (State) │ │ (Queue) │
└──────────┘ └──────────┘
```
## Component Breakdown
### 1. Bridge Module (`bridge/`)
**Purpose**: HTTP API layer providing authentication and authorization services
**Key Files**:
- `bridge.go` - Main bridge server initialization
- `config.go` - Configuration management
- `queue.go` - Asynq task queue setup and management
**Handlers** (`bridge/handlers/`):
#### Authentication & Authorization
- `auth.go` - General authentication handlers
- `oidc.go` - OpenID Connect provider implementation
- Discovery endpoint (`.well-known/openid-configuration`)
- Authorization endpoint with PKCE support
- Token endpoint with JWT generation
- UserInfo endpoint
- JWKS endpoint for key rotation
- `siop.go` - Self-Issued OpenID Provider (SIOP) flows
- DID-based authentication
- Verifiable presentation handling
- `webauthn.go` - WebAuthn registration and authentication
- Challenge generation
- Credential verification
- Device binding
#### OAuth2 Implementation
- `oauth2_provider.go` - Core OAuth2 provider
- Authorization code flow
- Client credentials flow
- Refresh token flow
- Token introspection
- Token revocation
- `oauth2_register.go` - Dynamic client registration (RFC 7591)
- `oauth2_clients.go` - Client management and validation
- `oauth2_delegation.go` - Token delegation flows
- `oauth2_token_exchange.go` - Token exchange (RFC 8693)
- `oauth2_scopes.go` - Scope validation and management
- `oauth2_security.go` - Security utilities (PKCE, rate limiting)
- `oauth2_types.go` - OAuth2 type definitions
#### Vault Operations
- `vault.go` - Vault operation handlers
- Generate vault enclaves
- Sign with vault
- Verify signatures
- Import/Export to IPFS
- Refresh vault state
#### Utility Handlers
- `broadcast.go` - Transaction broadcasting
- `health.go` - Health check endpoints
- `websocket.go` - WebSocket connection management
- `types.go` - Shared type definitions
### 2. Task Processing (`bridge/tasks/`)
**Purpose**: Asynchronous task definitions and processing
**Key Files**:
- `types.go` - Task type constants and definitions
- `generate.go` - Vault generation tasks
- `signing.go` - Signing operation tasks
- `attenuation.go` - UCAN token attenuation tasks
**Task Types**:
```go
const (
TypeVaultGenerate = "vault:generate"
TypeVaultSign = "vault:sign"
TypeVaultRefresh = "vault:refresh"
TypeUCANAttenuation = "ucan:attenuation"
)
```
**Queue Configuration**:
```go
Queues: map[string]int{
"critical": 6, // High priority tasks
"default": 3, // Normal priority tasks
"low": 1, // Low priority tasks
}
```
### 3. Main Service (`cmd/hway/`)
**Purpose**: Service entry point and initialization
**Key Responsibilities**:
1. Initialize Asynq server with Redis connection
2. Configure worker pools and queue priorities
3. Register task handlers
4. Start HTTP server (Echo)
5. Setup signal handling for graceful shutdown
**Configuration**:
```go
const (
RedisAddr = "127.0.0.1:6379"
PostgresAddr = "127.0.0.1:5432"
HTTPPort = ":8090"
WorkerConcurrency = 10
)
```
### 4. Database Migrations (`internal/migrations/`)
**Purpose**: PostgreSQL schema management
**Migration Files**:
- `001_accounts_table.sql` - User account storage
- `002_credentials_table.sql` - WebAuthn credential storage
- `003_profiles_table.sql` - User profile data
- `004_vaults_table.sql` - Vault state persistence
- `005_create_cosmos_registry.sql` - Cosmos chain registry
- `006_execute_cosmos_registry.sql` - Registry functions
- `007_webauthn_to_vc_func.sql` - WebAuthn to VC conversion
- `008_create_coinpaprika_market_data.sql` - Market data tables
- `009_webauthn_options_functions.sql` - WebAuthn helper functions
- `010_crypto_asset_symbol_linking.sql` - Asset metadata
- `011_common_functions.sql` - Shared SQL functions
- `012_crypto_coin_price_data.sql` - Price data storage
- `013_add_asset_quality_filters.sql` - Asset filtering
- `014_sessions_table.sql` - Session management
## Integration Points
### With Sonr Blockchain (`snrd`)
- **RPC/REST API**: Queries blockchain state via Cosmos SDK endpoints
- **Transaction Broadcasting**: Submits signed transactions to chain
- **DID Resolution**: Resolves DIDs from blockchain state
- **Vault State**: Stores vault metadata on-chain
### With Motor/Worker (WASM Plugin)
- **Task Execution**: Highway enqueues tasks, Motor executes via WASM
- **Vault Operations**: Motor provides cryptographic operations
- **Enclave Management**: Actor system manages WASM plugin lifecycle
### With Client Applications
- **OAuth2/OIDC**: Standard OAuth2 authorization flows
- **WebAuthn**: Browser-based passwordless authentication
- **WebSocket**: Real-time task status updates
- **SSE**: Server-Sent Events for progress tracking
### With External Services
- **IPFS**: Vault backup/restore operations
- **Redis**: Distributed task queue and caching
- **PostgreSQL**: Persistent state storage
## Key Features
### 1. OAuth2/OIDC Provider
- Full OAuth2 authorization server implementation
- OpenID Connect provider with ID tokens
- Dynamic client registration (RFC 7591)
- Token exchange (RFC 8693)
- PKCE support for public clients
- Refresh token rotation
- Token revocation and introspection
### 2. WebAuthn Support
- FIDO2/WebAuthn registration flows
- Authentication with platform authenticators
- Credential lifecycle management
- Challenge-response validation
- Attestation verification
### 3. Vault Task Processing
- Asynchronous cryptographic operations
- Priority-based queue management
- Actor-based concurrency model
- Retry logic with exponential backoff
- Task status tracking and notifications
### 4. UCAN Token Management
- UCAN token generation and signing
- Capability delegation and attenuation
- Token chain verification
- Integration with DID system
## Security Considerations
### Authentication & Authorization
- Multi-factor authentication support
- JWT token signing with RS256
- PKCE for authorization code flow
- Origin validation for WebAuthn
- Rate limiting on all endpoints
### Data Protection
- Password hashing with Argon2
- Encrypted vault data in PostgreSQL
- Secure token generation (crypto/rand)
- HTTPS-only in production
- CORS configuration
### Vault Security
- WASM sandbox isolation for cryptographic operations
- No private key exposure to server
- Encrypted backup to IPFS
- Session timeout and auto-lock
## Configuration
### Environment Variables
```bash
# Service Configuration
HIGHWAY_PORT=8090
LOG_LEVEL=info
# Database
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=hway
# Redis
REDIS_URL=redis://localhost:6379
# IPFS
IPFS_API_URL=http://localhost:5001
# OAuth2/OIDC
OIDC_ISSUER=http://localhost:8090
JWT_SIGNING_KEY_PATH=/path/to/private-key.pem
JWT_PUBLIC_KEY_PATH=/path/to/public-key.pem
# Security
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3100
SESSION_SECRET=change-me-in-production
```
### Asynq Configuration
```go
asynq.Config{
Concurrency: 10,
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
StrictPriority: false,
ErrorHandler: asynq.ErrorHandlerFunc(handleError),
Logger: slog.Default(),
}
```
## API Endpoints
### Authentication
- `POST /auth/register` - User registration
- `POST /auth/login` - User login
- `POST /auth/logout` - User logout
- `POST /auth/refresh` - Refresh access token
### OAuth2/OIDC
- `GET /.well-known/openid-configuration` - OIDC discovery
- `GET /oauth2/authorize` - Authorization endpoint
- `POST /oauth2/token` - Token endpoint
- `GET /oauth2/userinfo` - User info endpoint
- `GET /oauth2/jwks` - JSON Web Key Set
- `POST /oauth2/register` - Dynamic client registration
- `POST /oauth2/revoke` - Token revocation
- `POST /oauth2/introspect` - Token introspection
### WebAuthn
- `POST /webauthn/register/begin` - Start registration
- `POST /webauthn/register/finish` - Complete registration
- `POST /webauthn/login/begin` - Start authentication
- `POST /webauthn/login/finish` - Complete authentication
### Vault Operations
- `POST /vault/generate` - Generate new vault
- `POST /vault/sign` - Sign with vault
- `POST /vault/verify` - Verify signature
- `POST /vault/refresh` - Refresh vault state
- `POST /vault/export` - Export to IPFS
- `POST /vault/import` - Import from IPFS
### WebSocket
- `WS /ws/tasks/{task_id}` - Task status updates
### Health & Monitoring
- `GET /health` - Health check
- `GET /health/ready` - Readiness probe
- `GET /health/live` - Liveness probe
## Testing Strategy
### Unit Tests
```bash
go test ./bridge/...
go test ./bridge/handlers/...
go test ./bridge/tasks/...
```
### Integration Tests
```bash
# Requires PostgreSQL and Redis
INTEGRATION=true go test ./...
```
### E2E Tests
```bash
# Requires full stack (PostgreSQL, Redis, IPFS)
E2E=true go test ./e2e/...
```
## Dependencies
### Required Services
- PostgreSQL 14+
- Redis 7+
- IPFS node (for vault operations)
### Go Modules (Key Dependencies)
- `github.com/hibiken/asynq` - Distributed task queue
- `github.com/labstack/echo/v4` - HTTP framework
- `github.com/asynkron/protoactor-go` - Actor system
- `github.com/lib/pq` - PostgreSQL driver
- `github.com/go-webauthn/webauthn` - WebAuthn library
- `github.com/golang-jwt/jwt/v5` - JWT handling
- `github.com/redis/go-redis/v9` - Redis client
## Build & Deployment
### Build Commands
```bash
# Build binary
go build -o hway ./cmd/hway
# Build with specific tags
go build -tags production -o hway ./cmd/hway
# Build Docker image
docker build -t sonr-hway:latest .
```
### Docker Deployment
```yaml
services:
hway:
image: onsonr/hway:latest
environment:
POSTGRES_HOST: postgres
REDIS_URL: redis://redis:6379
depends_on:
- postgres
- redis
ports:
- "8090:8090"
```
## Migration Checklist
When setting up the new `sonr-io/hway` repository:
- [ ] Copy `cmd/hway/` directory
- [ ] Copy `bridge/` directory (all handlers and tasks)
- [ ] Copy `internal/migrations/` for database schema
- [ ] Update import paths from `github.com/sonr-io/sonr` to new repo
- [ ] Create standalone `go.mod` with required dependencies
- [ ] Setup CI/CD for independent releases
- [ ] Create Dockerfile for containerized deployment
- [ ] Document environment variables and configuration
- [ ] Add PostgreSQL and Redis setup instructions
- [ ] Create integration test suite with testcontainers
- [ ] Setup database migration tooling (e.g., golang-migrate)
- [ ] Configure monitoring and observability (Prometheus/Grafana)
- [ ] Document OAuth2 client registration process
- [ ] Create API documentation (OpenAPI/Swagger)
## Related Documentation
- OAuth2 RFC 6749: https://tools.ietf.org/html/rfc6749
- OpenID Connect Core: https://openid.net/specs/openid-connect-core-1_0.html
- Dynamic Client Registration RFC 7591: https://tools.ietf.org/html/rfc7591
- WebAuthn Spec: https://www.w3.org/TR/webauthn/
- Asynq Documentation: https://github.com/hibiken/asynq
- Proto.Actor: https://proto.actor/
+653
View File
@@ -0,0 +1,653 @@
# Motor (motr) Migration Context
> **Repository Migration**: `sonr-io/sonr` → `sonr-io/motr`
> **Components Moved**: `cmd/motr/`, `cmd/vault/`, `packages/`, `web/`
> **Note**: The `crypto/` library was moved to its own repository at `sonr-io/crypto`
## Overview
Motor (formerly "motr") is a multi-purpose WebAssembly service that provides:
1. **Worker**: WASM-based cryptographic vault operations (formerly "vault")
2. **Payment Gateway**: W3C Payment Handler API compliant payment processing
3. **TypeScript SDK**: Browser and Node.js client libraries for vault and payment operations
4. **Web Applications**: Authentication and dashboard web apps
The name "Motor" reflects its role as the execution engine powering secure operations in the Sonr ecosystem.
## Repository Structure
```
sonr-io/motr/
├── worker/ # WASM vault operations (Go → WASM)
│ ├── main.go # Entrypoint with WASM exports
│ ├── vault/ # Vault operation implementations
│ └── mpc/ # Multi-party computation
├── server/ # HTTP server mode (Go)
│ ├── main.go # HTTP/Payment Gateway server
│ ├── handlers/ # Payment & OIDC handlers
│ └── middleware/ # Security & rate limiting
├── packages/ # TypeScript SDK and libraries
│ ├── es/ # @motr/es - Core SDK
│ │ ├── client/ # Vault client
│ │ ├── worker/ # Service worker integration
│ │ ├── plugin/ # Plugin system
│ │ └── codec/ # Encoding/signing utilities
│ │
│ ├── sdk/ # @motr/sdk - High-level SDK
│ ├── ui/ # @motr/ui - UI components
│ └── com/ # @motr/com - Common utilities
└── web/ # Web applications
├── auth/ # Authentication app
└── dash/ # Dashboard app
```
## Dependencies
Motor depends on the **Sonr Cryptography Library** (`github.com/sonr-io/crypto`), which was moved to its own repository. See MIGRATE_CRYPTO.md for details on the crypto library.
## Component 1: Worker (WASM Vault)
**Technology**: Go 1.24.4 → WebAssembly via TinyGo
**Runtime**: Extism (WebAssembly plugin host)
**Purpose**: Secure cryptographic operations in sandboxed environment
### Architecture
```
┌──────────────────────────────────────────────┐
│ Host Application │
│ (Browser, Node.js, or Highway) │
└───────────────┬──────────────────────────────┘
┌───────────────┐
│ Extism │ ◄─── WASM Runtime
│ Runtime │
└───────┬───────┘
┌───────────────────────────────────────────────┐
│ worker.wasm (Motor Worker) │
├───────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Vault │ │ MPC │ │
│ │ Operations │ │ Enclave │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Crypto Primitives │ │
│ │ (Ed25519, ECDSA, BLS, etc.) │ │
│ └──────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────┘
```
### WASM Exports (Go Functions)
All exported functions follow the Extism PDK pattern:
#### Core Vault Operations
```go
//go:wasmexport generate
func generate() int32
// Creates new MPC enclave with key generation
// Input: GenerateRequest{id: string}
// Output: GenerateResponse{data: EnclaveData, public_key: []byte}
//go:wasmexport refresh
func refresh() int32
// Refreshes enclave cryptographic material
// Input: RefreshRequest{enclave: EnclaveData}
// Output: RefreshResponse{okay: bool, data: EnclaveData}
//go:wasmexport sign
func sign() int32
// Signs arbitrary message with vault key
// Input: SignRequest{message: []byte, enclave: EnclaveData}
// Output: SignResponse{signature: []byte}
//go:wasmexport verify
func verify() int32
// Verifies signature against public key
// Input: VerifyRequest{public_key: []byte, message: []byte, signature: []byte}
// Output: VerifyResponse{valid: bool}
```
#### Multi-Chain Transaction Signing
```go
//go:wasmexport sign_cosmos_transaction
func signCosmosTransaction() int32
// Signs Cosmos SDK transaction
// Input: CosmosSignRequest{chain_id, account_number, sequence, tx_bytes}
// Output: SignedTransaction{signature, signed_doc}
//go:wasmexport sign_evm_transaction
func signEvmTransaction() int32
// Signs Ethereum/EVM transaction
// Input: EVMSignRequest{chain_id, nonce, tx_data, gas_limit}
// Output: SignedTransaction{v, r, s, raw_tx}
//go:wasmexport sign_message
func signMessage() int32
// Signs arbitrary message (EIP-191/EIP-712)
// Input: MessageSignRequest{message, encoding_type}
// Output: MessageSignature{signature, recovery_id}
```
#### Vault Import/Export (IPFS)
```go
//go:wasmexport export
func export() int32
// Exports encrypted vault to IPFS
// Input: ExportRequest{enclave: EnclaveData, password: []byte}
// Output: ExportResponse{cid: string, success: bool}
//go:wasmexport import
func import() int32
// Imports encrypted vault from IPFS
// Input: ImportRequest{cid: string, password: []byte}
// Output: ImportResponse{enclave: EnclaveData, success: bool}
```
#### WebAuthn Integration
```go
//go:wasmexport create_vault_enclave
func createVaultEnclave() int32
// Creates vault with WebAuthn configuration
// Input: VaultConfig{vault_id, webauthn_enabled, auto_lock_timeout}
// Output: VaultEnclave{enclave_data, webauthn_credentials}
//go:wasmexport unlock_vault
func unlockVault() int32
// Unlocks vault with WebAuthn or password
// Input: UnlockRequest{vault_id, auth_method, credentials}
// Output: UnlockResponse{success, session_token}
//go:wasmexport lock_vault
func lockVault() int32
// Locks vault and clears sensitive data
// Input: LockRequest{vault_id}
// Output: LockResponse{success}
```
#### Health & Monitoring
```go
//go:wasmexport get_vault_health
func getVaultHealth() int32
// Returns vault health status
// Output: EnclaveHealth{vault_id, status, last_activity, key_rotation_due}
//go:wasmexport get_version
func getVersion() int32
// Returns worker version and capabilities
// Output: VersionInfo{version, supported_chains, features}
```
### MPC Enclave Structure
```go
type EnclaveData struct {
ID string `json:"id"`
PublicKey []byte `json:"public_key"`
PrivateKeyShare []byte `json:"private_key_share"` // Encrypted
Threshold uint32 `json:"threshold"`
Parties uint32 `json:"parties"`
ChainID string `json:"chain_id"`
CreatedAt int64 `json:"created_at"`
LastRefresh int64 `json:"last_refresh"`
Metadata map[string]string `json:"metadata"`
}
```
**Note**: Motor relies on the Sonr Cryptography Library (`github.com/sonr-io/crypto`) for all cryptographic operations. The crypto library provides comprehensive primitives including Ed25519, ECDSA, BLS signatures, MPC, threshold cryptography, and more. See `MIGRATE_CRYPTO.md` for the complete crypto library documentation.
### Build Configuration
```bash
# Build WASM module with TinyGo
tinygo build -o worker.wasm -target wasi \
-no-debug \
-opt 2 \
-scheduler none \
./worker/main.go
# Optimize with wasm-opt
wasm-opt -O3 -o worker.optimized.wasm worker.wasm
# Build with Extism toolchain
extism compile worker.wasm -o worker.plugin.wasm
```
### IPFS Integration
```go
// IPFS Configuration
const (
IPFSStorageEndpoint = "http://127.0.0.1:5001/api/v0/add"
IPFSRetrievalEndpoint = "http://127.0.0.1:5001/api/v0/cat"
IPFSGateway = "https://ipfs.did.run/ipfs/"
)
// Export vault to IPFS
func ExportToIPFS(enclave *EnclaveData, password []byte) (cid string, error) {
// 1. Serialize enclave data
// 2. Encrypt with AES-256-GCM using password
// 3. Upload to IPFS
// 4. Return content ID (CID)
}
```
## Component 2: Server (Payment Gateway)
**Technology**: Go 1.24.4 HTTP Server
**Framework**: `go-wasm-http-server` (can run as service worker or HTTP)
**Purpose**: Payment processing and OIDC authorization
### Features
#### W3C Payment Handler API
- Payment request processing
- Card validation (Luhn algorithm, CVV, expiry)
- PCI DSS compliant tokenization
- Transaction signing with HMAC-SHA256
- AES-256-GCM encryption for card data
- Refund processing
- Audit logging
#### OIDC Authorization Server
- Full OpenID Connect provider
- Discovery endpoint
- Authorization with PKCE
- Token endpoint (JWT generation)
- UserInfo endpoint
- JWKS endpoint
- Refresh tokens
#### Security
- Rate limiting (100 req/min per client)
- Origin validation
- Security headers (CSP, X-Frame-Options)
- CORS configuration
- Secure token generation
- Card number masking
### API Endpoints
```
POST /api/payment/process - Process payment
POST /api/payment/validate - Validate payment method
POST /api/payment/refund - Process refund
GET /.well-known/openid-configuration
GET /oauth2/authorize
POST /oauth2/token
GET /oauth2/userinfo
GET /oauth2/jwks
```
## Component 3: TypeScript SDK (`packages/`)
**Purpose**: Browser and Node.js integration for Motor services
### Package Structure
#### `@motr/es` (Core SDK)
**Client Module** (`client/`):
- `VaultClient`: Main vault operations client
- `VaultClientWithIPFS`: IPFS-enabled vault client
- RPC/REST API integration
- Transaction broadcasting
**Worker Module** (`worker/`):
- `MotorServiceWorkerManager`: Service worker lifecycle
- `MotorClient`: HTTP client for Motor API
- Payment gateway client
- OIDC client integration
**Plugin Module** (`plugin/`):
- Plugin loading and caching
- WASM module verification
- Enclave storage (IndexedDB, localStorage)
- IPFS pinning integration
**Codec Module** (`codec/`):
- Address encoding (Bech32, EIP-55)
- Key management
- Transaction signing
- Signature verification
- Message serialization
**Auth Module** (`auth/`):
- WebAuthn registration
- WebAuthn authentication
- Credential management
- Passkey integration
**Generated Protobufs** (`protobufs/`):
- Cosmos SDK types
- Sonr blockchain types
- IBC types
- CosmWasm types
#### `@motr/sdk` (High-Level SDK)
- Simplified API wrappers
- Common operation helpers
- Error handling utilities
- TypeScript type definitions
#### `@motr/ui` (UI Components)
- React components for vault operations
- WebAuthn UI flows
- Payment forms
- Dashboard widgets
#### `@motr/com` (Common Utilities)
- Validation helpers
- Formatting utilities
- Type definitions
- Constants
### Usage Examples
```typescript
// Initialize vault client
import { createVaultClient } from '@motr/es';
const client = await createVaultClient({
rpcUrl: 'http://localhost:26657',
restUrl: 'http://localhost:1317',
});
// Generate vault
const vault = await client.generate({ id: 'my-vault' });
// Sign message
const signature = await client.sign({
message: new Uint8Array([1, 2, 3]),
enclave: vault.data,
});
// Export to IPFS
const cid = await client.export({
enclave: vault.data,
password: new Uint8Array([/* password */]),
});
```
```typescript
// Service worker integration
import { registerMotorServiceWorker } from '@motr/es';
const registration = await registerMotorServiceWorker({
workerUrl: '/worker.js',
scope: '/motor',
});
// Use in browser
const plugin = await createMotorPlugin({
auto_register_worker: true,
prefer_service_worker: true,
});
await plugin.processPayment({
amount: 100.00,
currency: 'USD',
method: 'card',
});
```
## Component 4: Web Applications
### Authentication App (`web/auth/`)
**Framework**: Next.js 14+
**Purpose**: WebAuthn registration and OIDC flows
**Features**:
- Passkey registration UI
- Login flows
- Session management
- OIDC client implementation
- WebAuthn ceremony handling
**Tech Stack**:
- Next.js (App Router)
- React 18
- TailwindCSS
- Fumadocs (documentation)
- Sonr UI components
### Dashboard App (`web/dash/`)
**Framework**: Next.js 14+
**Purpose**: Vault management and blockchain interaction
**Features**:
- Vault creation and management
- Transaction signing UI
- DID document viewer
- DWN record browser
- Token management
- Network switcher
**Tech Stack**:
- Next.js (App Router)
- React 18
- TailwindCSS
- @motr/sdk for blockchain interaction
- Charts and visualizations
### Shared Configuration
Both apps use:
- `@motr/ui` for shared components
- `@motr/sdk` for blockchain operations
- Environment-based configuration
- SSR/SSG optimization
- Edge runtime compatibility
## Build & Development
### Worker (WASM)
```bash
# Build worker WASM
make worker
# Or with TinyGo directly
tinygo build -o worker.wasm -target wasi ./worker/main.go
```
### Server (Payment Gateway)
```bash
# Build server
go build -o motr-server ./server/main.go
# Run server
./motr-server --port 8080
```
### TypeScript SDK
```bash
# Install dependencies
pnpm install
# Build all packages
pnpm -r build
# Build specific package
pnpm --filter @motr/es build
# Run tests
pnpm test
# Generate from protobufs
cd packages/es
pnpm gen:protobufs
```
### Web Applications
```bash
# Development
pnpm --filter @motr/auth dev
pnpm --filter @motr/dash dev
# Build
pnpm --filter @motr/auth build
pnpm --filter @motr/dash build
# Production
pnpm --filter @motr/auth start
```
## Testing Strategy
### Go (Worker)
```bash
# Unit tests
go test ./worker/...
go test ./server/...
# With race detection
go test -race ./...
# Coverage
go test -cover ./...
```
### TypeScript (SDK/Apps)
```bash
# Unit tests
pnpm test
# E2E tests
pnpm test:e2e
# Type checking
pnpm typecheck
```
### Integration Tests
```bash
# Requires Motor server running
INTEGRATION=true pnpm test
```
## Configuration
### Worker Environment Variables
```bash
# WASM runtime configuration (via Extism)
CHAIN_ID=sonr-testnet-1
PASSWORD=default-password
IPFS_GATEWAY=https://ipfs.did.run/ipfs/
```
### SDK Configuration
```typescript
interface MotorConfig {
// RPC endpoints
rpcUrl: string;
restUrl: string;
// IPFS
ipfsGateways: string[];
enableIPFSPersistence: boolean;
// Service worker
workerUrl: string;
preferServiceWorker: boolean;
// Security
timeout: number;
maxRetries: number;
}
```
### Web App Environment Variables
```bash
# Common
NODE_ENV=production
NEXT_PUBLIC_CHAIN_ID=sonr-testnet-1
# Auth App
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_WEBAUTHN_RP_ID=localhost
NEXT_PUBLIC_WEBAUTHN_RP_NAME="Sonr Auth"
# Dashboard App
NEXT_PUBLIC_RPC_ENDPOINT=http://localhost:26657
NEXT_PUBLIC_REST_ENDPOINT=http://localhost:1317
NEXT_PUBLIC_IPFS_GATEWAY=https://ipfs.io/ipfs/
```
## Migration Checklist
When setting up the new `sonr-io/motr` repository:
### Worker
- [ ] Copy `cmd/vault/``worker/`
- [ ] Update import paths to use `github.com/sonr-io/crypto`
- [ ] Create `worker/go.mod` with crypto dependency
- [ ] Setup TinyGo build scripts
- [ ] Add WASM optimization pipeline
- [ ] Document export functions and types
- [ ] Create test suite for WASM functions
### Server
- [ ] Copy `cmd/motr/``server/`
- [ ] Update payment gateway handlers
- [ ] Configure OIDC provider
- [ ] Setup rate limiting
- [ ] Add monitoring/metrics
- [ ] Create Dockerfile
- [ ] Document API endpoints
### TypeScript SDK
- [ ] Copy `packages/` directory
- [ ] Update package names to `@motr/*`
- [ ] Setup pnpm workspace
- [ ] Configure build pipeline (tsup/rollup)
- [ ] Setup Biome for linting/formatting
- [ ] Generate types from protobuf
- [ ] Create comprehensive tests
- [ ] Setup Changesets for versioning
- [ ] Publish to npm registry
### Web Applications
- [ ] Copy `web/` directory
- [ ] Update dependencies to use `@motr/*`
- [ ] Configure environment variables
- [ ] Setup build and deployment
- [ ] Create Docker images
- [ ] Add E2E tests
- [ ] Document user flows
### General
- [ ] Create monorepo structure
- [ ] Setup CI/CD pipelines
- [ ] Configure release automation
- [ ] Create API documentation
- [ ] Write migration guide
- [ ] Setup npm organization (@motr)
- [ ] Configure security scanning
- [ ] Setup monitoring and logging
## Related Documentation
- TinyGo: https://tinygo.org/
- Extism: https://extism.org/
- W3C Payment Handler API: https://www.w3.org/TR/payment-handler/
- WebAuthn: https://www.w3.org/TR/webauthn/
- IPFS: https://docs.ipfs.tech/
- Next.js: https://nextjs.org/
- pnpm Workspaces: https://pnpm.io/workspaces
+13 -82
View File
@@ -104,7 +104,7 @@ stop:
########################################
### Tools & dependencies
########################################
format: go-format ts-format
format: go-format
go-format:
@gum log --level info "Formatting Go code with gofumpt and goimports..."
@if command -v gofumpt > /dev/null; then \
@@ -119,12 +119,7 @@ go-format:
fi
@gum log --level info "✅ Code formatted"
ts-format:
@gum log --level info "Formatting all web applications..."
@pnpm biome format --config-path=$(PWD)/biome.json .
lint: go-lint ts-lint
lint: go-lint
go-lint:
@gum log --level info "Running golangci-lint..."
@@ -137,11 +132,7 @@ go-lint:
golangci-lint run --timeout=10m; \
fi
ts-lint:
@gum log --level info "Running Biome linter with auto-fix for TypeScript projects..."
@pnpm biome lint --config-path=./biome.json --write .
.PHONY: lint go-lint ts-lint format
.PHONY: lint go-lint format
go-mod-cache: go.sum
@gum log --level info "Download go modules to local cache"
@@ -152,10 +143,6 @@ go.sum: go.mod
@go mod tidy
@go mod verify
pnpm-install:
@gum log --level info "Installing pnpm dependencies"
@pnpm install
draw-deps:
@# requires brew install graphviz or apt-get install graphviz
go install github.com/RobotsAndPencils/goviz@latest
@@ -165,18 +152,11 @@ draw-deps:
tidy:
@go mod tidy
@make -C client tidy
@make -C crypto tidy
@make -C cmd/hway tidy
@make -C cmd/motr tidy
@make -C cmd/vault tidy
clean: tidy
@gum log --level info "Cleaning build artifacts..."
rm -rf snapcraft-local.yaml build/ dist/
@$(MAKE) -C cmd/snrd clean
@$(MAKE) -C cmd/hway clean
@$(MAKE) -C cmd/vault clean
@$(MAKE) -C cmd/motr clean
clean-docker:
@gum log --level info "Removing all Docker volumes and networks..."
@@ -197,28 +177,19 @@ build: go.sum
build-snrd: build
build-client: go.sum build-vault build-motr
build-client: go.sum
@$(MAKE) -C client build
@cd /tmp && go mod init test || true
@cd /tmp && go get github.com/sonr-io/sonr/client@main || true
@cd /tmp && gum log --level info "Client SDK import successful"
build-hway: go.sum build-vault build-motr
@$(MAKE) -C cmd/hway build
build-vault:
@$(MAKE) -C cmd/vault build
build-motr: ## Build Motor WASM service worker
@$(MAKE) -C cmd/motr build
# Build all components in parallel
build-all: go.sum
@gum log --level info "Building all components in parallel..."
@$(MAKE) -j5 build build-hway build-client build-motr build-vault
@$(MAKE) -j2 build build-client
@gum log --level info "✅ All components built successfully"
.PHONY: install build build-client build-hway build-snrd build-vault build-motr build-all
.PHONY: install build build-client build-snrd build-all
########################################
### Docker & Services
@@ -245,18 +216,12 @@ dockernet:
########################################
# Smart component release detection and automation
release:
@$(MAKE) -C cmd/hway release
@$(MAKE) -C cmd/motr release
@$(MAKE) -C cmd/snrd release
@$(MAKE) -C cmd/vault release
# Snapshot builds for development
snapshot:
@gum log --level info "📦 Preparing component snapshot..."
@$(MAKE) -C cmd/vault snapshot
@$(MAKE) -C cmd/motr snapshot
@$(MAKE) -C cmd/snrd snapshot
@$(MAKE) -C cmd/hway snapshot
.PHONY: release snapshot
@@ -290,31 +255,22 @@ test-build-snrd: build
@chmod +x build/snrd
@./build/snrd version
test-build-hway: build-hway
@ls -la build/hway
test-tdd:
go test -json ./... 2>&1 | tdd-guard-go -project-root ${GIT_ROOT}
test-app:
@VERSION=$(VERSION) go test -C . -mod=readonly -tags='ledger test_ledger_mock test' github.com/sonr-io/sonr/app/... github.com/sonr-io/sonr/x/... github.com/sonr-io/sonr/types/... github.com/sonr-io/sonr/internal/...
@VERSION=$(VERSION) CGO_LDFLAGS="-lm" go test -C . -mod=readonly -tags='ledger test_ledger_mock test' github.com/sonr-io/sonr/app/... github.com/sonr-io/sonr/x/... github.com/sonr-io/common/... github.com/sonr-io/sonr/internal/...
test-devops:
@echo "No devops tests"
test-client:
@$(MAKE) -C cmd/vault build
@$(MAKE) -C client test
test-crypto:
@$(MAKE) -C crypto test
test-dwn-ci:
@$(MAKE) -C cmd/vault build
@go test -mod=readonly -tags='ledger test_ledger_mock test' -run='!IPFS' ./x/dwn/...
test-internal:
@$(MAKE) -C cmd/vault build
@VERSION=$(VERSION) go test -mod=readonly -tags='ledger test_ledger_mock test' ./internal/...
# Module testing - Simplified
@@ -340,28 +296,14 @@ test-module:
VERSION=$(VERSION) go test -mod=readonly -tags='ledger test_ledger_mock test' ./x/$(MODULE)/...; \
fi \
fi
# Specialized tests
test-hway:
@gum log --level info "Testing Highway service..."
@VERSION=$(VERSION) go test -C . -mod=readonly -v github.com/sonr-io/sonr/bridge/...
test-proto:
@$(MAKE) -C proto lint
@$(MAKE) -C proto check-breaking
test-motr:
@gum log --level info "Testing Motor WASM service worker..."
@$(MAKE) -C cmd/motr test
test-vault:
@gum log --level info "Testing Vault WASM plugin..."
@$(MAKE) -C cmd/vault test
test-benchmark:
@go test -mod=readonly -bench=. ./...
.PHONY: test test-all test-unit test-race test-cover test-tdd test-module test-hway test-motr test-vault test-benchmark
.PHONY: test test-all test-unit test-race test-cover test-tdd test-module test-benchmark
###############################################################################
### Protobuf ###
@@ -373,12 +315,6 @@ climd-gen:
proto-gen:
@gum log --level info "Generating Go protobuf files..."
@$(MAKE) -C proto gen
@gum log --level info "Generating TypeScript protobuf files..."
@if [ -d "packages/es" ]; then \
cd packages/es && pnpm gen:protobufs || { gum log --level error "TypeScript protobuf generation failed"; exit 1; }; \
else \
gum log --level warn "Skipping TypeScript generation: packages/es not found"; \
fi
@gum log --level info "Auto-formatting generated protobuf files..."
@$(MAKE) format
@@ -400,8 +336,6 @@ swagger-gen:
done
@gum log --level info "Cleaning up empty source directories..."
@find docs/static/openapi -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
@gum log --level info "Converting Swagger 2.0 to OpenAPI 3.0..."
@pnpm run convert-swagger
@gum log --level info "✅ API documentation processing complete."
templ-gen:
@@ -444,14 +378,15 @@ help:
@gum log --level info ""
@gum log --level info "🛠️ Build & Install:"
@gum log --level info " install Install snrd binary"
@gum log --level info " build Build snrd binary with vault WASM"
@gum log --level info " build Build snrd binary"
@gum log --level info " build-all Build all components in parallel"
@gum log --level info " build-hway Build Highway service"
@gum log --level info " build-vault Build vault WASM module"
@gum log --level info " build-motr Build Motor WASM service worker"
@gum log --level info " build-client Build client SDK"
@gum log --level info " docker Build Docker images"
@gum log --level info ""
@gum log --level info "📦 Release & Distribution:"
@gum log --level info " release Create production release with GoReleaser"
@gum log --level info " snapshot Create development snapshot builds"
@gum log --level info ""
@gum log --level info "🚀 Local Development:"
@gum log --level info " localnet Start single-node testnet"
@gum log --level info " start Start backend services"
@@ -476,10 +411,6 @@ help:
@gum log --level info " test-e2e Run e2e tests"
@gum log --level info " test-e2e-all Run all e2e tests"
@gum log --level info " test-module Test specific module (MODULE=did|dwn|svc)"
@gum log --level info " test-packages Test all packages (lint + build)"
@gum log --level info " test-ipfs Test vault with IPFS export/import"
@gum log --level info " test-vault Test vault operations"
@gum log --level info " test-web Test all web apps (lint + build)"
@gum log --level info ""
@gum log --level info "📚 Module Testing Examples:"
@gum log --level info " make test-module MODULE=did # Test DID module"
+2 -2
View File
@@ -6,8 +6,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/crypto/keys"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/crypto/keys"
"github.com/sonr-io/crypto/ucan"
)
// UCANDecorator validates UCAN tokens in transactions
+1 -1
View File
@@ -9,7 +9,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/crypto/ucan"
)
// TestUCANDecorator tests the UCAN decorator functionality
+1 -1
View File
@@ -13,7 +13,7 @@ import (
genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
"github.com/sonr-io/sonr/app"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
"github.com/spf13/cobra"
)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"path/filepath"
"github.com/sonr-io/sonr/app"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
"github.com/spf13/cobra"
)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/client"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
)
// SonrContext manages node-specific state and configuration
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"cosmossdk.io/log"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
)
// TestSonrContextInitialization tests SonrContext initialization with VRF keys
-103
View File
@@ -1,103 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"useExhaustiveDependencies": "warn"
},
"style": {
"noNonNullAssertion": "warn",
"useConst": "error"
},
"suspicious": {
"noExplicitAny": "warn",
"noArrayIndexKey": "warn"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
},
"overrides": [
{
"formatter": {
"enabled": false
},
"linter": {
"enabled": false
}
},
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
},
"complexity": {
"noStaticOnlyClass": "off"
}
}
}
},
{
"linter": {
"rules": {
"style": {
"useTemplate": "warn",
"noUnusedTemplateLiteral": "warn"
}
}
}
},
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
}
}
}
},
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
}
}
}
}
]
}
-118
View File
@@ -1,118 +0,0 @@
// Package bridge provides the HTTP bridge server for the Highway service.
package bridge
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/bridge/server"
)
// HighwayService encapsulates the entire Highway service setup and lifecycle
type HighwayService struct {
config *Config
client *asynq.Client
httpServer *server.Server
queueManager *QueueManager
// Internal channels for coordination
ctx context.Context
cancel context.CancelFunc
sigCh chan os.Signal
}
// NewHighwayService creates a new Highway service with all components initialized
func NewHighwayService() *HighwayService {
// Setup graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
// Handle shutdown signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
// Initialize configuration
config := NewConfig()
// Initialize Redis-based task queue client
client := asynq.NewClient(asynq.RedisClientOpt{Addr: config.RedisAddr})
log.Printf("Asynq client connected to Redis at %s", config.RedisAddr)
// Create server configuration for bridge proxy
serverConfig := &server.Config{
HTTPAddr: fmt.Sprintf(":%d", config.HTTPPort),
JWTSecret: config.JWTSecret,
IPFSClient: config.IPFSClient,
}
// Create HTTP bridge server
httpServer := server.NewServer(serverConfig)
// Initialize UCAN task processing server with queue manager
queueManager := NewQueueManager(config)
return &HighwayService{
config: config,
client: client,
httpServer: httpServer,
queueManager: queueManager,
ctx: ctx,
cancel: cancel,
sigCh: sigCh,
}
}
// Start begins the Highway service with HTTP server and queue processing
func (hs *HighwayService) Start() error {
log.Println("Starting Highway Service - UCAN-based MPC Task Processor")
// Create and start HTTP bridge server in a goroutine
go func() {
log.Printf("Starting HTTP bridge server on port %d", hs.config.HTTPPort)
if err := hs.httpServer.Start(hs.client); err != nil {
log.Fatalf("HTTP bridge server failed: %v", err)
}
}()
// Start queue server with graceful shutdown support
go func() {
if err := hs.queueManager.Run(); err != nil {
log.Printf("Asynq server error: %v", err)
hs.cancel()
}
}()
// Wait for shutdown signal
select {
case <-hs.sigCh:
log.Println("Received shutdown signal, initiating graceful shutdown...")
case <-hs.ctx.Done():
log.Println("Context cancelled, shutting down...")
}
return nil
}
// Shutdown gracefully stops all service components
func (hs *HighwayService) Shutdown() {
log.Println("Shutting down Highway service...")
// Close Asynq client
if hs.client != nil {
hs.client.Close()
}
// Shutdown queue manager
if hs.queueManager != nil {
hs.queueManager.Shutdown()
}
// Cancel context to signal shutdown to all components
hs.cancel()
log.Println("Highway service stopped")
}
-31
View File
@@ -1,31 +0,0 @@
package bridge
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewHighwayService(t *testing.T) {
service := NewHighwayService()
require.NotNil(t, service)
assert.NotNil(t, service.config)
assert.NotNil(t, service.client)
assert.NotNil(t, service.httpServer)
assert.NotNil(t, service.queueManager)
assert.NotNil(t, service.ctx)
assert.NotNil(t, service.cancel)
assert.NotNil(t, service.sigCh)
}
func TestHighwayServiceComponents(t *testing.T) {
service := NewHighwayService()
defer service.Shutdown()
// Test that all components are properly initialized
assert.NotNil(t, service.config.RedisAddr)
assert.NotNil(t, service.config.JWTSecret)
assert.NotNil(t, service.config.AsynqConfig)
}
-202
View File
@@ -1,202 +0,0 @@
package bridge
import (
"log"
"os"
"strconv"
"time"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/types/ipfs"
)
const (
DefaultRedisAddr = "127.0.0.1:6379"
DefaultJWTSecret = "highway-ucan-secret-key"
DefaultHTTPPort = 8090
ShutdownTimeout = 30 * time.Second
)
// OIDCProviderConfig contains OIDC provider configuration
type OIDCProviderConfig struct {
Issuer string
PublicURL string
SigningKeyPath string
EncryptionKeyPath string
AuthorizationCodeTTL time.Duration
AccessTokenTTL time.Duration
RefreshTokenTTL time.Duration
IDTokenTTL time.Duration
EnablePKCE bool
EnableRefreshTokens bool
EnableSIOP bool
SupportedScopes []string
SupportedResponseTypes []string
SupportedGrantTypes []string
AllowedRedirectURIs []string
WebAuthnRPID string
WebAuthnRPName string
WebAuthnTimeout int
AutoCreateVault bool
}
type Config struct {
RedisAddr string
HTTPPort int
JWTSecret []byte
IPFSClient ipfs.IPFSClient
ShutdownTimeout time.Duration
AsynqConfig asynq.Config
OIDC OIDCProviderConfig
}
func NewConfig() *Config {
jwtSecret := initializeJWTSecret()
redisAddr := getRedisAddr()
httpPort := getHTTPPort()
ipfsClient := initializeIPFS()
oidcConfig := initializeOIDCConfig()
return &Config{
RedisAddr: redisAddr,
HTTPPort: httpPort,
JWTSecret: jwtSecret,
IPFSClient: ipfsClient,
ShutdownTimeout: ShutdownTimeout,
AsynqConfig: asynq.Config{
Concurrency: 10,
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
ShutdownTimeout: ShutdownTimeout,
// Enhanced error handling and retry configuration
RetryDelayFunc: asynq.DefaultRetryDelayFunc,
IsFailure: func(err error) bool {
return err != nil
},
},
OIDC: oidcConfig,
}
}
func initializeJWTSecret() []byte {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
secret = DefaultJWTSecret
log.Printf("Warning: Using default JWT secret for UCAN operations")
log.Printf("Set JWT_SECRET environment variable for production deployment")
} else {
log.Println("JWT secret loaded from environment")
}
return []byte(secret)
}
func getRedisAddr() string {
// Check for REDIS_URL first (Docker Compose style)
if url := os.Getenv("REDIS_URL"); url != "" {
// Parse redis://host:port format
if len(url) > 8 && url[:8] == "redis://" {
return url[8:]
}
return url
}
// Fall back to REDIS_ADDR
if addr := os.Getenv("REDIS_ADDR"); addr != "" {
return addr
}
log.Printf("Using default Redis address: %s", DefaultRedisAddr)
return DefaultRedisAddr
}
func initializeIPFS() ipfs.IPFSClient {
ipfsClient, err := ipfs.GetClient()
if err != nil {
log.Printf("Warning: IPFS client initialization failed: %v", err)
log.Println("Enclave data will be handled directly without IPFS storage")
return nil
}
log.Println("IPFS client initialized successfully")
return ipfsClient
}
func initializeOIDCConfig() OIDCProviderConfig {
issuer := os.Getenv("OIDC_ISSUER")
if issuer == "" {
issuer = "https://localhost:8080"
}
publicURL := os.Getenv("OIDC_PUBLIC_URL")
if publicURL == "" {
publicURL = issuer
}
rpID := os.Getenv("WEBAUTHN_RP_ID")
if rpID == "" {
rpID = "localhost"
}
rpName := os.Getenv("WEBAUTHN_RP_NAME")
if rpName == "" {
rpName = "Sonr Identity Platform"
}
return OIDCProviderConfig{
Issuer: issuer,
PublicURL: publicURL,
SigningKeyPath: os.Getenv("OIDC_SIGNING_KEY_PATH"),
EncryptionKeyPath: os.Getenv("OIDC_ENCRYPTION_KEY_PATH"),
AuthorizationCodeTTL: 10 * time.Minute,
AccessTokenTTL: 1 * time.Hour,
RefreshTokenTTL: 7 * 24 * time.Hour,
IDTokenTTL: 1 * time.Hour,
EnablePKCE: true,
EnableRefreshTokens: true,
EnableSIOP: true,
SupportedScopes: []string{
"openid", "profile", "email", "did", "vault", "offline_access",
},
SupportedResponseTypes: []string{
"code", "id_token", "code id_token",
},
SupportedGrantTypes: []string{
"authorization_code", "refresh_token", "client_credentials",
},
AllowedRedirectURIs: getRedirectURIs(),
WebAuthnRPID: rpID,
WebAuthnRPName: rpName,
WebAuthnTimeout: 60000,
AutoCreateVault: true,
}
}
// getRedirectURIs returns the allowed redirect URIs for OIDC
func getRedirectURIs() []string {
// Default URIs for development
uris := []string{
"http://localhost:3000/callback",
"http://localhost:3001/callback",
"https://localhost:3000/callback",
"https://localhost:3001/callback",
}
// Add additional URIs from environment if specified
if envURIs := os.Getenv("OIDC_ALLOWED_REDIRECT_URIS"); envURIs != "" {
// Parse comma-separated URIs
// This could be enhanced with proper validation
log.Printf("Additional redirect URIs configured from environment")
}
return uris
}
// getHTTPPort returns the HTTP port for the Highway service
func getHTTPPort() int {
if port := os.Getenv("HIGHWAY_PORT"); port != "" {
if p, err := strconv.Atoi(port); err == nil {
return p
}
}
return DefaultHTTPPort
}
-265
View File
@@ -1,265 +0,0 @@
// Package handlers provides HTTP handlers for the highway server
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// CustomClaims defines custom JWT claims for vault operations
type CustomClaims struct {
UserID string `json:"user_id"`
Permissions []string `json:"permissions"`
jwt.RegisteredClaims
}
// LoginRequest represents the login request payload
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// LoginResponse represents the login response payload
type LoginResponse struct {
Token string `json:"token"`
}
// LoginHandler generates JWT tokens for authentication
// This now supports both traditional login and OIDC-based authentication
func LoginHandler(jwtSecret []byte) echo.HandlerFunc {
return func(c echo.Context) error {
// Check if this is an OIDC callback
code := c.QueryParam("code")
if code != "" {
return handleOIDCCallback(c, code, jwtSecret)
}
// Check if user has WebAuthn credentials
authHeader := c.Request().Header.Get("X-WebAuthn-Assertion")
if authHeader != "" {
return handleWebAuthnLogin(c, authHeader, jwtSecret)
}
// Traditional login flow
var req LoginRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
// Simple authentication - in production, validate against a proper user database
if req.Username == "" || req.Password == "" {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Username and password are required"},
)
}
// For demo purposes, accept any non-empty credentials
// In production, verify credentials against database/directory
if req.Username == "vault-user" && req.Password == "vault-pass" {
// Create custom claims
claims := &CustomClaims{
UserID: req.Username,
Permissions: []string{
"vault:generate",
"vault:sign",
"vault:verify",
"vault:export",
"vault:import",
"vault:refresh",
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 24 hours
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "highway-vault",
Subject: req.Username,
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign token with secret
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to generate token"},
)
}
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
}
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
}
}
// handleOIDCCallback processes OIDC authorization code callback
func handleOIDCCallback(c echo.Context, code string, jwtSecret []byte) error {
// Exchange code for tokens using OIDC token endpoint
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: code,
RedirectURI: c.QueryParam("redirect_uri"),
ClientID: c.QueryParam("client_id"),
}
// Call internal OIDC token handler
// In production, this would make an HTTP call to the OIDC provider
c.Set("oidc_token_request", tokenReq)
if err := handleAuthorizationCodeGrant(c, tokenReq); err != nil {
return err
}
// Get the OIDC session from context
session, ok := c.Get("oidc_session").(*OIDCSession)
if !ok {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to establish OIDC session",
})
}
// Create JWT token from OIDC session
claims := &CustomClaims{
UserID: session.UserDID,
Permissions: []string{
"vault:generate",
"vault:sign",
"vault:verify",
"vault:export",
"vault:import",
"vault:refresh",
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(session.ExpiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "highway-vault-oidc",
Subject: session.UserDID,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate token",
})
}
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
}
// handleWebAuthnLogin processes WebAuthn-based login
func handleWebAuthnLogin(c echo.Context, assertion string, jwtSecret []byte) error {
// Verify WebAuthn assertion
// This would integrate with the WebAuthn handlers
// For now, extract username from assertion (simplified)
username := c.Request().Header.Get("X-WebAuthn-Username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "WebAuthn username required",
})
}
// Verify the assertion matches a stored credential
webAuthnStore.mu.RLock()
credentials, exists := webAuthnStore.credentials[username]
webAuthnStore.mu.RUnlock()
if !exists || len(credentials) == 0 {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "No WebAuthn credentials found for user",
})
}
// Create JWT token for WebAuthn authenticated user
claims := &CustomClaims{
UserID: username,
Permissions: []string{
"vault:generate",
"vault:sign",
"vault:verify",
"vault:export",
"vault:import",
"vault:refresh",
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "highway-vault-webauthn",
Subject: username,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate token",
})
}
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
}
// OIDCLoginHandler initiates OIDC login flow
func OIDCLoginHandler() echo.HandlerFunc {
return func(c echo.Context) error {
// Build authorization URL
authURL := buildOIDCAuthorizationURL(
c.QueryParam("client_id"),
c.QueryParam("redirect_uri"),
c.QueryParam("scope"),
c.QueryParam("state"),
)
// Redirect to OIDC authorization endpoint
return c.Redirect(http.StatusFound, authURL)
}
}
// buildOIDCAuthorizationURL constructs the OIDC authorization URL
func buildOIDCAuthorizationURL(clientID, redirectURI, scope, state string) string {
// Default values if not provided
if clientID == "" {
clientID = "highway-vault-client"
}
if redirectURI == "" {
redirectURI = "http://localhost:8080/auth/callback"
}
if scope == "" {
scope = "openid profile did vault"
}
if state == "" {
state = generateState()
}
// Build authorization URL
return fmt.Sprintf(
"https://localhost:8080/oidc/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
clientID,
redirectURI,
scope,
state,
)
}
// generateState generates a random state parameter for OIDC
func generateState() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "default-state"
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
-60
View File
@@ -1,60 +0,0 @@
package handlers
import (
"encoding/json"
"testing"
"github.com/sonr-io/sonr/crypto/mpc"
)
// getQueueFromPriority returns the queue name based on priority
func getQueueFromPriority(priority string) string {
return GetQueueFromPriority(priority)
}
// BenchmarkJSONMarshaling measures JSON encoding/decoding performance
func BenchmarkJSONMarshaling(b *testing.B) {
payload := map[string]any{
"message": []byte("benchmark test message for JSON marshaling performance"),
"enclave": &mpc.EnclaveData{},
"priority": "critical",
}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Marshal
data, err := json.Marshal(payload)
if err != nil {
b.Error(err)
}
// Unmarshal
var decoded map[string]any
err = json.Unmarshal(data, &decoded)
if err != nil {
b.Error(err)
}
}
})
}
// BenchmarkQueuePrioritySelection measures queue selection performance
func BenchmarkQueuePrioritySelection(b *testing.B) {
priorities := []string{"critical", "high", "default", "low", "", "unknown"}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
priority := priorities[i%len(priorities)]
i++
queue := getQueueFromPriority(priority)
_ = queue // Avoid compiler optimization
}
})
}
-333
View File
@@ -1,333 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
)
// BroadcastService handles blockchain broadcasting operations
type BroadcastService struct {
// TODO: Add cosmos SDK client for actual broadcasting
}
var broadcastService = &BroadcastService{}
// HandleBroadcast handles generic message broadcasting to blockchain
func HandleBroadcast(c echo.Context) error {
var req BroadcastRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid broadcast request",
})
}
// Validate request
if req.Message == nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Message is required",
})
}
// Process based on gasless flag
if req.Gasless {
return handleGaslessBroadcast(c, &req)
}
return handleStandardBroadcast(c, &req)
}
// handleGaslessBroadcast handles gasless transaction broadcasting
func handleGaslessBroadcast(c echo.Context, req *BroadcastRequest) error {
// Validate gasless eligibility
if !isGaslessEligible(req.Message) {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Message not eligible for gasless broadcast",
})
}
// TODO: Implement actual blockchain broadcast
// For now, simulate successful broadcast
response := &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12345,
Code: 0,
RawLog: "Transaction broadcast successfully",
Success: true,
}
return c.JSON(http.StatusOK, response)
}
// handleStandardBroadcast handles regular transaction broadcasting
func handleStandardBroadcast(c echo.Context, req *BroadcastRequest) error {
// Get sender address
fromAddress := req.FromAddress
if fromAddress == "" {
// Try to get from context
userDID := c.Get("user_did")
if userDID != nil {
fromAddress = deriveAddressFromDID(userDID.(string))
}
}
if fromAddress == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "From address is required for standard broadcast",
})
}
// TODO: Implement actual blockchain broadcast
// For now, simulate successful broadcast
response := &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12346,
Code: 0,
RawLog: "Transaction broadcast successfully",
Success: true,
}
return c.JSON(http.StatusOK, response)
}
// BroadcastWebAuthnRegistration broadcasts WebAuthn registration to blockchain
func BroadcastWebAuthnRegistration(
credential *WebAuthnCredential,
gasless bool,
) (*BroadcastResponse, error) {
// Create MsgRegisterWebAuthnCredential
msg := map[string]any{
"@type": "/sonr.did.v1.MsgRegisterWebAuthnCredential",
"username": credential.Username,
"credential_id": credential.CredentialID,
"public_key": credential.PublicKey,
"attestation_object": credential.AttestationObject,
"client_data_json": credential.ClientDataJSON,
"origin": credential.Origin,
"algorithm": credential.Algorithm,
}
// Create broadcast request (for future use with actual broadcast)
_ = &BroadcastRequest{
Message: msg,
Gasless: gasless,
AutoSign: true,
}
// Simulate broadcast (TODO: Implement actual broadcast)
return &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12347,
Code: 0,
RawLog: "WebAuthn credential registered successfully",
Success: true,
}, nil
}
// BroadcastVaultCreation broadcasts vault creation to blockchain
func BroadcastVaultCreation(
userDID string,
vaultConfig map[string]any,
) (*BroadcastResponse, error) {
// Create MsgCreateVault
msg := map[string]any{
"@type": "/sonr.vault.v1.MsgCreateVault",
"creator": userDID,
"config": vaultConfig,
}
// Create broadcast request (for future use with actual broadcast)
_ = &BroadcastRequest{
Message: msg,
Gasless: true, // Vault creation is gasless for new users
AutoSign: true,
FromAddress: deriveAddressFromDID(userDID),
}
// Simulate broadcast (TODO: Implement actual broadcast)
return &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12348,
Code: 0,
RawLog: "Vault created successfully",
Success: true,
}, nil
}
// BroadcastDIDDocument broadcasts DID document to blockchain
func BroadcastDIDDocument(didDoc map[string]any, gasless bool) (*BroadcastResponse, error) {
// Create MsgCreateDIDDocument or MsgUpdateDIDDocument
msg := map[string]any{
"@type": "/sonr.did.v1.MsgCreateDIDDocument",
"did_document": didDoc,
}
// Create broadcast request (for future use with actual broadcast)
_ = &BroadcastRequest{
Message: msg,
Gasless: gasless,
AutoSign: true,
}
// Simulate broadcast (TODO: Implement actual broadcast)
return &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12349,
Code: 0,
RawLog: "DID document created successfully",
Success: true,
}, nil
}
// HandleTransactionStatus checks transaction status
func HandleTransactionStatus(c echo.Context) error {
txHash := c.Param("hash")
if txHash == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Transaction hash is required",
})
}
// TODO: Query actual blockchain for transaction status
// For now, return simulated status
status := map[string]any{
"tx_hash": txHash,
"height": 12350,
"status": "confirmed",
"code": 0,
"gas_used": 50000,
"gas_wanted": 100000,
"timestamp": time.Now().Unix(),
}
return c.JSON(http.StatusOK, status)
}
// HandleEstimateGas estimates gas for a transaction
func HandleEstimateGas(c echo.Context) error {
var req BroadcastRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request",
})
}
// Check if gasless eligible
if isGaslessEligible(req.Message) {
return c.JSON(http.StatusOK, map[string]any{
"gas_estimate": 0,
"gasless": true,
"fee": "0usnr",
})
}
// TODO: Implement actual gas estimation
// For now, return default estimate
return c.JSON(http.StatusOK, map[string]any{
"gas_estimate": 100000,
"gasless": false,
"fee": "100usnr",
})
}
// Helper functions
// isGaslessEligible checks if a message is eligible for gasless broadcasting
func isGaslessEligible(message any) bool {
// Check message type
msgMap, ok := message.(map[string]any)
if !ok {
return false
}
msgType, ok := msgMap["@type"].(string)
if !ok {
return false
}
// WebAuthn registration and vault creation are gasless
gaslessTypes := []string{
"/sonr.did.v1.MsgRegisterWebAuthnCredential",
"/sonr.vault.v1.MsgCreateVault",
"/sonr.did.v1.MsgCreateDIDDocument",
}
for _, t := range gaslessTypes {
if msgType == t {
return true
}
}
return false
}
// deriveAddressFromDID derives a blockchain address from a DID
func deriveAddressFromDID(did string) string {
// TODO: Implement actual address derivation
// For now, return a placeholder address
return "sonr1placeholder" + did[len(did)-10:]
}
// generateTxHash generates a mock transaction hash
func generateTxHash() string {
// TODO: Replace with actual tx hash from broadcast
return fmt.Sprintf("%X", time.Now().UnixNano())
}
// CreateWebAuthnBroadcastHandler creates a handler that broadcasts WebAuthn credentials
func CreateWebAuthnBroadcastHandler() echo.HandlerFunc {
return func(c echo.Context) error {
// Get WebAuthn credential from context
credential, ok := c.Get("webauthn_credential").(*WebAuthnCredential)
if !ok {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No WebAuthn credential found",
})
}
// Broadcast to blockchain
response, err := BroadcastWebAuthnRegistration(credential, true)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, response)
}
}
// CreateVaultBroadcastHandler creates a handler that broadcasts vault creation
func CreateVaultBroadcastHandler() echo.HandlerFunc {
return func(c echo.Context) error {
// Get user DID from context
userDID, ok := c.Get("user_did").(string)
if !ok {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "User DID not found",
})
}
// Get vault config from request
var vaultConfig map[string]any
if err := c.Bind(&vaultConfig); err != nil {
// Use default config
vaultConfig = map[string]any{
"type": "standard",
"encryption": "AES256",
}
}
// Broadcast vault creation
response, err := BroadcastVaultCreation(userDID, vaultConfig)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, response)
}
}
-33
View File
@@ -1,33 +0,0 @@
package handlers
import "errors"
// Common errors for OAuth2 and UCAN handling
var (
// Client errors
ErrClientNotFound = errors.New("client not found")
ErrInvalidClientCredentials = errors.New("invalid client credentials")
// Token errors
ErrTokenNotFound = errors.New("token not found")
ErrTokenExpired = errors.New("token has expired")
ErrInvalidToken = errors.New("invalid token")
ErrTokenRevoked = errors.New("token has been revoked")
// Authorization errors
ErrUnauthorized = errors.New("unauthorized")
ErrInsufficientScope = errors.New("insufficient scope")
ErrInvalidScope = errors.New("invalid scope")
ErrScopeNotAllowed = errors.New("scope not allowed for client")
// UCAN errors
ErrInvalidAttenuation = errors.New("invalid attenuation")
ErrInvalidDelegation = errors.New("invalid delegation")
ErrBrokenChain = errors.New("broken delegation chain")
ErrPrivilegeEscalation = errors.New("privilege escalation attempted")
// OIDC errors
ErrInvalidRedirectURI = errors.New("invalid redirect URI")
ErrInvalidResponseType = errors.New("invalid response type")
ErrInvalidGrantType = errors.New("invalid grant type")
)
-27
View File
@@ -1,27 +0,0 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetQueueFromPriority(t *testing.T) {
tests := []struct {
priority string
expected string
}{
{"critical", "critical"},
{"high", "critical"},
{"low", "low"},
{"", "default"},
{"unknown", "default"},
}
for _, tt := range tests {
t.Run(tt.priority, func(t *testing.T) {
result := GetQueueFromPriority(tt.priority)
assert.Equal(t, tt.expected, result)
})
}
}
-201
View File
@@ -1,201 +0,0 @@
package handlers
import (
"context"
"net/http"
"sync"
"time"
"github.com/hibiken/asynq"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/types/ipfs"
)
// HealthStatus represents the health status of the service
type HealthStatus struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Uptime string `json:"uptime"`
Dependencies map[string]string `json:"dependencies"`
}
// HealthChecker manages health and readiness checks
type HealthChecker struct {
startTime time.Time
redisClient *asynq.Client
ipfsClient ipfs.IPFSClient
ready bool
readyMu sync.RWMutex
redisHealthy bool
ipfsHealthy bool
}
// NewHealthChecker creates a new health checker
func NewHealthChecker(redisClient *asynq.Client, ipfsClient ipfs.IPFSClient) *HealthChecker {
hc := &HealthChecker{
startTime: time.Now(),
redisClient: redisClient,
ipfsClient: ipfsClient,
ready: false,
}
// Start background health checks
go hc.startHealthChecks()
return hc
}
// startHealthChecks runs periodic health checks
func (hc *HealthChecker) startHealthChecks() {
// Initial startup delay
time.Sleep(3 * time.Second)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
// Run initial check
hc.checkDependencies()
for range ticker.C {
hc.checkDependencies()
}
}
// checkDependencies checks all service dependencies
func (hc *HealthChecker) checkDependencies() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Check Redis connectivity
hc.redisHealthy = hc.checkRedis(ctx)
// Check IPFS connectivity (optional)
hc.ipfsHealthy = hc.checkIPFS(ctx)
// Update readiness based on critical dependencies
hc.readyMu.Lock()
hc.ready = hc.redisHealthy // Redis is required
hc.readyMu.Unlock()
}
// checkRedis verifies Redis connectivity
func (hc *HealthChecker) checkRedis(ctx context.Context) bool {
if hc.redisClient == nil {
return false
}
// Try to ping Redis by enqueuing a test task
testTask := asynq.NewTask("health:check", nil)
_, err := hc.redisClient.EnqueueContext(ctx, testTask,
asynq.Queue("health"),
asynq.MaxRetry(0),
asynq.Retention(1*time.Second)) // Auto-delete after 1 second
if err != nil {
return false
}
return true
}
// checkIPFS verifies IPFS connectivity
func (hc *HealthChecker) checkIPFS(ctx context.Context) bool {
if hc.ipfsClient == nil {
return true // IPFS is optional
}
// Check IPFS connectivity by getting version
ch := make(chan bool, 1)
go func() {
// Try a simple operation to check connectivity
if hc.ipfsClient != nil {
// IPFS client exists, assume healthy for now
// Real check would depend on actual IPFS client implementation
ch <- true
} else {
ch <- false
}
}()
select {
case result := <-ch:
return result
case <-ctx.Done():
return false
}
}
// IsReady returns whether the service is ready to handle requests
func (hc *HealthChecker) IsReady() bool {
hc.readyMu.RLock()
defer hc.readyMu.RUnlock()
return hc.ready
}
// GetStatus returns the current health status
func (hc *HealthChecker) GetStatus() HealthStatus {
uptime := time.Since(hc.startTime)
deps := make(map[string]string)
if hc.redisHealthy {
deps["redis"] = "healthy"
} else {
deps["redis"] = "unhealthy"
}
if hc.ipfsClient != nil {
if hc.ipfsHealthy {
deps["ipfs"] = "healthy"
} else {
deps["ipfs"] = "unhealthy"
}
} else {
deps["ipfs"] = "not_configured"
}
status := "healthy"
if !hc.ready {
status = "unhealthy"
}
return HealthStatus{
Status: status,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Uptime: uptime.String(),
Dependencies: deps,
}
}
// Global health checker instance
var healthChecker *HealthChecker
// InitHealthChecker initializes the global health checker
func InitHealthChecker(redisClient *asynq.Client, ipfsClient ipfs.IPFSClient) {
healthChecker = NewHealthChecker(redisClient, ipfsClient)
}
// HealthCheckHandler returns health status (liveness probe)
func HealthCheckHandler(c echo.Context) error {
if healthChecker == nil {
return c.JSON(http.StatusOK, map[string]string{"status": "starting"})
}
status := healthChecker.GetStatus()
if status.Status == "healthy" {
return c.JSON(http.StatusOK, status)
}
return c.JSON(http.StatusServiceUnavailable, status)
}
// ReadinessHandler returns readiness status (readiness probe)
func ReadinessHandler(c echo.Context) error {
if healthChecker == nil || !healthChecker.IsReady() {
return c.JSON(http.StatusServiceUnavailable, map[string]string{
"ready": "false",
"reason": "service not ready",
})
}
return c.JSON(http.StatusOK, map[string]string{
"ready": "true",
})
}
-422
View File
@@ -1,422 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"strings"
"sync"
"time"
)
const (
// ClientTypePublic represents a public OAuth2 client
ClientTypePublic = "public"
// ClientTypeConfidential represents a confidential OAuth2 client
ClientTypeConfidential = "confidential"
)
// ClientRegistry manages OAuth2 client registrations
type ClientRegistry struct {
mu sync.RWMutex
clients map[string]*OAuth2Client
}
// NewClientRegistry creates a new client registry
func NewClientRegistry() *ClientRegistry {
registry := &ClientRegistry{
clients: make(map[string]*OAuth2Client),
}
// Initialize with default clients for development
registry.initializeDefaultClients()
return registry
}
// initializeDefaultClients adds default clients for development/testing
func (r *ClientRegistry) initializeDefaultClients() {
// Development public client (e.g., SPA)
_ = r.RegisterClient(&OAuth2Client{
ClientID: "sonr-web-app",
ClientType: ClientTypePublic,
RedirectURIs: []string{
"http://localhost:3000/callback",
"http://localhost:3001/callback",
},
AllowedScopes: []string{
"openid",
"profile",
"vault:read",
"vault:write",
"service:manage",
},
AllowedGrants: []string{"authorization_code", "refresh_token"},
TokenLifetime: time.Hour,
RequirePKCE: true,
TrustedClient: true,
RequiresConsent: false,
Metadata: map[string]string{
"name": "Sonr Web Application",
"description": "Official Sonr web application",
"logo_uri": "https://sonr.io/logo.png",
"client_uri": "https://app.sonr.io",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
// Development confidential client (e.g., backend service)
_ = r.RegisterClient(&OAuth2Client{
ClientID: "sonr-backend-service",
ClientSecret: "development-secret-change-in-production",
ClientType: ClientTypeConfidential,
RedirectURIs: []string{"http://localhost:8081/callback"},
AllowedScopes: []string{"openid", "profile", "vault:admin", "service:manage"},
AllowedGrants: []string{"authorization_code", "refresh_token", "client_credentials"},
TokenLifetime: time.Hour * 2,
RequirePKCE: false,
TrustedClient: true,
RequiresConsent: false,
Metadata: map[string]string{
"name": "Sonr Backend Service",
"description": "Backend service for Sonr ecosystem",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
// Example third-party client
_ = r.RegisterClient(&OAuth2Client{
ClientID: "example-third-party",
ClientSecret: "third-party-secret",
ClientType: ClientTypeConfidential,
RedirectURIs: []string{"https://example.com/oauth/callback"},
AllowedScopes: []string{"openid", "profile", "vault:read"},
AllowedGrants: []string{"authorization_code", "refresh_token"},
TokenLifetime: time.Hour,
RequirePKCE: true,
TrustedClient: false,
RequiresConsent: true,
Metadata: map[string]string{
"name": "Example Third Party App",
"description": "Example integration partner",
"logo_uri": "https://example.com/logo.png",
"client_uri": "https://example.com",
"policy_uri": "https://example.com/privacy",
"tos_uri": "https://example.com/terms",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
}
// RegisterClient registers a new OAuth2 client
func (r *ClientRegistry) RegisterClient(client *OAuth2Client) error {
r.mu.Lock()
defer r.mu.Unlock()
// Validate client
if err := r.validateClient(client); err != nil {
return err
}
// Generate client ID if not provided
if client.ClientID == "" {
client.ClientID = generateOAuth2ClientID()
}
// Generate client secret for confidential clients
if client.ClientType == ClientTypeConfidential && client.ClientSecret == "" {
client.ClientSecret = generateClientSecret()
}
// Set timestamps
if client.CreatedAt.IsZero() {
client.CreatedAt = time.Now()
}
client.UpdatedAt = time.Now()
// Store client
r.clients[client.ClientID] = client
return nil
}
// GetClient retrieves a client by ID
func (r *ClientRegistry) GetClient(clientID string) (*OAuth2Client, error) {
r.mu.RLock()
defer r.mu.RUnlock()
client, exists := r.clients[clientID]
if !exists {
return nil, fmt.Errorf("client not found: %s", clientID)
}
return client, nil
}
// UpdateClient updates an existing client
func (r *ClientRegistry) UpdateClient(clientID string, updates *OAuth2Client) error {
r.mu.Lock()
defer r.mu.Unlock()
client, exists := r.clients[clientID]
if !exists {
return fmt.Errorf("client not found: %s", clientID)
}
// Update allowed fields
if len(updates.RedirectURIs) > 0 {
client.RedirectURIs = updates.RedirectURIs
}
if len(updates.AllowedScopes) > 0 {
client.AllowedScopes = updates.AllowedScopes
}
if len(updates.AllowedGrants) > 0 {
client.AllowedGrants = updates.AllowedGrants
}
if updates.TokenLifetime > 0 {
client.TokenLifetime = updates.TokenLifetime
}
if updates.Metadata != nil {
client.Metadata = updates.Metadata
}
client.RequirePKCE = updates.RequirePKCE
client.RequiresConsent = updates.RequiresConsent
client.UpdatedAt = time.Now()
return nil
}
// DeleteClient removes a client from the registry
func (r *ClientRegistry) DeleteClient(clientID string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.clients[clientID]; !exists {
return fmt.Errorf("client not found: %s", clientID)
}
delete(r.clients, clientID)
return nil
}
// ListClients returns all registered clients
func (r *ClientRegistry) ListClients() []*OAuth2Client {
r.mu.RLock()
defer r.mu.RUnlock()
clients := make([]*OAuth2Client, 0, len(r.clients))
for _, client := range r.clients {
clients = append(clients, client)
}
return clients
}
// ValidateRedirectURI checks if a redirect URI is valid for the client
func (c *OAuth2Client) ValidateRedirectURI(redirectURI string) bool {
if redirectURI == "" {
return false
}
// Parse the redirect URI
parsedURI, err := url.Parse(redirectURI)
if err != nil {
return false
}
// Check against registered redirect URIs
for _, registeredURI := range c.RedirectURIs {
registeredParsed, err := url.Parse(registeredURI)
if err != nil {
continue
}
// For public clients, allow localhost with any port
if c.ClientType == ClientTypePublic && registeredParsed.Hostname() == "localhost" &&
parsedURI.Hostname() == "localhost" {
if registeredParsed.Path == parsedURI.Path {
return true
}
}
// Exact match for other cases
if registeredURI == redirectURI {
return true
}
// Allow subdomain matching for trusted clients
if c.TrustedClient && matchesWithSubdomain(registeredParsed, parsedURI) {
return true
}
}
return false
}
// ValidateScopes checks if the requested scopes are allowed for the client
func (c *OAuth2Client) ValidateScopes(requestedScopes []string) bool {
if len(requestedScopes) == 0 {
return true // No scopes requested is valid
}
for _, scope := range requestedScopes {
if !c.hasScope(scope) {
return false
}
}
return true
}
// hasScope checks if a client has a specific scope
func (c *OAuth2Client) hasScope(scope string) bool {
for _, allowedScope := range c.AllowedScopes {
if allowedScope == scope {
return true
}
// Check for hierarchical scopes (e.g., vault:admin includes vault:read)
if isHierarchicalScope(allowedScope, scope) {
return true
}
}
return false
}
// HasGrantType checks if a client supports a specific grant type
func (c *OAuth2Client) HasGrantType(grantType string) bool {
for _, allowed := range c.AllowedGrants {
if allowed == grantType {
return true
}
}
return false
}
// validateClient validates client configuration
func (r *ClientRegistry) validateClient(client *OAuth2Client) error {
// Validate client type
if client.ClientType != ClientTypePublic && client.ClientType != ClientTypeConfidential {
return fmt.Errorf("invalid client type: %s", client.ClientType)
}
// Validate redirect URIs
if len(client.RedirectURIs) == 0 {
return fmt.Errorf("at least one redirect URI is required")
}
for _, uri := range client.RedirectURIs {
if _, err := url.Parse(uri); err != nil {
return fmt.Errorf("invalid redirect URI: %s", uri)
}
}
// Validate grant types
if len(client.AllowedGrants) == 0 {
client.AllowedGrants = []string{"authorization_code"}
}
validGrants := map[string]bool{
"authorization_code": true,
"implicit": true,
"refresh_token": true,
"client_credentials": true,
"password": true,
"urn:ietf:params:oauth:grant-type:device_code": true,
}
for _, grant := range client.AllowedGrants {
if !validGrants[grant] {
return fmt.Errorf("invalid grant type: %s", grant)
}
}
// Client credentials grant requires confidential client
if contains(client.AllowedGrants, "client_credentials") &&
client.ClientType != ClientTypeConfidential {
return fmt.Errorf("client_credentials grant requires confidential client")
}
// Public clients should use PKCE
if client.ClientType == ClientTypePublic && !client.RequirePKCE {
// Log warning but don't fail
fmt.Printf("Warning: Public client %s should use PKCE\n", client.ClientID)
}
return nil
}
// Helper functions
func generateOAuth2ClientID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return fmt.Sprintf("client_%s", base64.RawURLEncoding.EncodeToString(bytes))
}
func generateClientSecret() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func matchesWithSubdomain(registered, requested *url.URL) bool {
if registered.Scheme != requested.Scheme {
return false
}
if registered.Path != requested.Path {
return false
}
// Check if requested hostname is a subdomain of registered
registeredHost := registered.Hostname()
requestedHost := requested.Hostname()
if registeredHost == requestedHost {
return true
}
// Check subdomain match (e.g., *.example.com matches sub.example.com)
if strings.HasPrefix(registeredHost, "*.") {
domain := strings.TrimPrefix(registeredHost, "*.")
return strings.HasSuffix(requestedHost, domain)
}
return false
}
func isHierarchicalScope(allowed, requested string) bool {
// Define scope hierarchy
hierarchy := map[string][]string{
"vault:admin": {"vault:write", "vault:read", "vault:sign"},
"vault:write": {"vault:read"},
"service:manage": {"service:read", "service:write"},
"did:write": {"did:read"},
}
childScopes, exists := hierarchy[allowed]
if !exists {
return false
}
for _, child := range childScopes {
if child == requested {
return true
}
// Recursive check for nested hierarchies
if isHierarchicalScope(child, requested) {
return true
}
}
return false
}
-323
View File
@@ -1,323 +0,0 @@
package handlers
import (
"encoding/json"
"fmt"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// UCANDelegator handles UCAN token generation for OAuth flows
type UCANDelegator struct {
scopeMapper *ScopeMapper
signer UCANSigner
}
// UCANSigner interface for signing UCAN tokens
type UCANSigner interface {
Sign(token *ucan.Token) (string, error)
GetIssuerDID() string
}
// NewUCANDelegator creates a new UCAN delegator
func NewUCANDelegator(signer UCANSigner) *UCANDelegator {
if signer == nil {
// Use blockchain signer by default
signer, _ = NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
}
return &UCANDelegator{
scopeMapper: NewScopeMapper(),
signer: signer,
}
}
// CreateDelegation creates a UCAN token for user-to-client delegation
func (d *UCANDelegator) CreateDelegation(
userDID string,
clientID string,
scopes []string,
expiresAt time.Time,
) (*ucan.Token, error) {
// Build resource context for the user
resourceContext := d.buildResourceContext(userDID)
// Map OAuth scopes to UCAN attenuations
attenuations := d.scopeMapper.MapToUCAN(scopes, userDID, clientID, resourceContext)
if len(attenuations) == 0 {
return nil, fmt.Errorf("no valid attenuations for scopes: %v", scopes)
}
// Create UCAN token
token := &ucan.Token{
Issuer: userDID,
Audience: clientID,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, "user_delegation"),
},
},
}
// Sign the token
signedToken, err := d.signToken(token)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
token.Raw = signedToken
return token, nil
}
// CreateServiceDelegation creates a UCAN token for service-to-service delegation
func (d *UCANDelegator) CreateServiceDelegation(
clientID string,
scopes []string,
expiresAt time.Time,
) (*ucan.Token, error) {
// Service delegations use the OAuth provider as issuer
issuerDID := d.signer.GetIssuerDID()
// Build resource context for service
resourceContext := map[string]string{
"service_id": clientID,
"type": "service",
}
// Map OAuth scopes to UCAN attenuations
attenuations := d.scopeMapper.MapToUCAN(scopes, issuerDID, clientID, resourceContext)
if len(attenuations) == 0 {
return nil, fmt.Errorf("no valid attenuations for scopes: %v", scopes)
}
// Create UCAN token
token := &ucan.Token{
Issuer: issuerDID,
Audience: clientID,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, "service_delegation"),
},
},
}
// Sign the token
signedToken, err := d.signToken(token)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
token.Raw = signedToken
return token, nil
}
// CreateDelegationChain creates a chain of UCAN tokens for complex delegations
func (d *UCANDelegator) CreateDelegationChain(
userDID string,
intermediaries []string,
finalAudience string,
scopes []string,
expiresAt time.Time,
) ([]*ucan.Token, error) {
chain := make([]*ucan.Token, 0, len(intermediaries)+1)
currentIssuer := userDID
proofs := []ucan.Proof{}
// Create delegation for each intermediary
for _, intermediary := range intermediaries {
token, err := d.createIntermediateDelegation(
currentIssuer,
intermediary,
scopes,
expiresAt,
proofs,
)
if err != nil {
return nil, fmt.Errorf("failed to create intermediate delegation: %w", err)
}
chain = append(chain, token)
proofs = append(proofs, ucan.Proof(token.Raw))
currentIssuer = intermediary
}
// Create final delegation
finalToken, err := d.createFinalDelegation(
currentIssuer,
finalAudience,
scopes,
expiresAt,
proofs,
)
if err != nil {
return nil, fmt.Errorf("failed to create final delegation: %w", err)
}
chain = append(chain, finalToken)
return chain, nil
}
// RevokeDelegation revokes a UCAN delegation
func (d *UCANDelegator) RevokeDelegation(tokenID string) error {
// TODO: Implement delegation revocation
// This would typically involve:
// 1. Adding the token to a revocation list
// 2. Publishing revocation to a public ledger or database
// 3. Notifying relevant parties
return fmt.Errorf("delegation revocation not yet implemented")
}
// ValidateDelegation validates a UCAN token delegation
func (d *UCANDelegator) ValidateDelegation(token *ucan.Token, requiredScopes []string) error {
// Check expiration
if time.Now().Unix() > token.ExpiresAt {
return fmt.Errorf("token expired")
}
// Check not before
if token.NotBefore > 0 && time.Now().Unix() < token.NotBefore {
return fmt.Errorf("token not yet valid")
}
// Verify the token has required capabilities
for _, scope := range requiredScopes {
if !d.tokenGrantsScope(token, scope) {
return fmt.Errorf("token does not grant required scope: %s", scope)
}
}
// TODO: Verify signature
// TODO: Verify delegation chain if proofs exist
return nil
}
// Private helper methods
func (d *UCANDelegator) buildResourceContext(userDID string) map[string]string {
// TODO: Fetch actual resource information for the user
return map[string]string{
"vault_address": fmt.Sprintf("vault:%s", userDID),
"enclave_cid": fmt.Sprintf("cid:%s", userDID),
"dwn_id": fmt.Sprintf("dwn:%s", userDID),
"service_id": fmt.Sprintf("service:%s", userDID),
}
}
func (d *UCANDelegator) createOAuthFact(scopes []string, delegationType string) json.RawMessage {
fact := map[string]any{
"oauth_scopes": scopes,
"delegation_type": delegationType,
"issued_at": time.Now().Unix(),
"oauth_version": "2.0",
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
func (d *UCANDelegator) signToken(token *ucan.Token) (string, error) {
if d.signer == nil {
return "", fmt.Errorf("no signer configured")
}
return d.signer.Sign(token)
}
func (d *UCANDelegator) createIntermediateDelegation(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
) (*ucan.Token, error) {
return d.createDelegationWithType(
issuer,
audience,
scopes,
expiresAt,
proofs,
"intermediate_delegation",
)
}
func (d *UCANDelegator) createFinalDelegation(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
) (*ucan.Token, error) {
return d.createDelegationWithType(
issuer,
audience,
scopes,
expiresAt,
proofs,
"final_delegation",
)
}
// createDelegationWithType creates a delegation token with a specific type
func (d *UCANDelegator) createDelegationWithType(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
delegationType string,
) (*ucan.Token, error) {
resourceContext := d.buildResourceContext(issuer)
attenuations := d.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
token := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, delegationType),
},
},
}
signedToken, err := d.signToken(token)
if err != nil {
return nil, err
}
token.Raw = signedToken
return token, nil
}
func (d *UCANDelegator) tokenGrantsScope(token *ucan.Token, scope string) bool {
// Get the scope definition
scopeDef, exists := d.scopeMapper.GetScope(scope)
if !exists {
return false
}
// Check if any attenuation grants the required capabilities
for _, attenuation := range token.Attenuations {
if attenuation.Capability.Grants(scopeDef.UCANActions) {
// Also check resource type matches
if d.resourceMatches(attenuation.Resource, scopeDef.ResourceType) {
return true
}
}
}
return false
}
func (d *UCANDelegator) resourceMatches(resource ucan.Resource, resourceType string) bool {
// Simple scheme matching for now
return resource.GetScheme() == resourceType || resource.GetScheme() == "*"
}
-923
View File
@@ -1,923 +0,0 @@
package handlers
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/crypto/ucan"
)
// OAuth2Provider extends OIDCProvider with full OAuth2 capabilities
type OAuth2Provider struct {
*OIDCProvider
clientRegistry *ClientRegistry
scopeMapper *ScopeMapper
ucanDelegator *UCANDelegator
authCodeStore *AuthCodeStore
accessTokenStore *AccessTokenStore
refreshTokenStore *RefreshTokenStore
consentStore *ConsentStore
config *OAuth2Config
}
// AuthCodeStore manages authorization codes
type AuthCodeStore struct {
mu sync.RWMutex
codes map[string]*OAuth2AuthorizationCode
}
// AccessTokenStore manages access tokens
type AccessTokenStore struct {
mu sync.RWMutex
tokens map[string]*OAuth2AccessToken
}
// RefreshTokenStore manages refresh tokens
type RefreshTokenStore struct {
mu sync.RWMutex
tokens map[string]*OAuth2RefreshToken
}
// ConsentStore manages user consent records
type ConsentStore struct {
mu sync.RWMutex
consents map[string]*UserConsent // key: userDID:clientID
}
// UserConsent represents stored user consent
type UserConsent struct {
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
ApprovedScopes []string `json:"approved_scopes"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
}
var oauth2Provider *OAuth2Provider
// InitializeOAuth2Provider initializes the OAuth2 provider
func InitializeOAuth2Provider() {
oauth2Provider = &OAuth2Provider{
OIDCProvider: oidcProvider,
clientRegistry: NewClientRegistry(),
scopeMapper: NewScopeMapper(),
ucanDelegator: NewUCANDelegator(nil),
authCodeStore: &AuthCodeStore{codes: make(map[string]*OAuth2AuthorizationCode)},
accessTokenStore: &AccessTokenStore{tokens: make(map[string]*OAuth2AccessToken)},
refreshTokenStore: &RefreshTokenStore{tokens: make(map[string]*OAuth2RefreshToken)},
consentStore: &ConsentStore{consents: make(map[string]*UserConsent)},
config: getDefaultOAuth2Config(),
}
// Start cleanup goroutine for expired tokens
go oauth2Provider.cleanupExpiredTokens()
}
// GetOAuth2Discovery returns OAuth2 discovery configuration
func GetOAuth2Discovery(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
return c.JSON(http.StatusOK, oauth2Provider.config)
}
// HandleOAuth2Authorize handles OAuth2 authorization requests
func HandleOAuth2Authorize(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
req := &OAuth2AuthorizationRequest{
ResponseType: c.QueryParam("response_type"),
ClientID: c.QueryParam("client_id"),
RedirectURI: c.QueryParam("redirect_uri"),
Scope: c.QueryParam("scope"),
State: c.QueryParam("state"),
CodeChallenge: c.QueryParam("code_challenge"),
CodeChallengeMethod: c.QueryParam("code_challenge_method"),
Nonce: c.QueryParam("nonce"),
Prompt: c.QueryParam("prompt"),
LoginHint: c.QueryParam("login_hint"),
}
// Validate client
client, err := oauth2Provider.clientRegistry.GetClient(req.ClientID)
if err != nil {
return oauth2Error(c, "invalid_client", "Unknown client", req.State)
}
// Validate redirect URI
if !client.ValidateRedirectURI(req.RedirectURI) {
return oauth2Error(c, "invalid_request", "Invalid redirect URI", req.State)
}
// Validate response type
if !isValidResponseType(req.ResponseType) {
return redirectError(
c,
req.RedirectURI,
"unsupported_response_type",
"Response type not supported",
req.State,
)
}
// Validate scopes
requestedScopes := parseScopes(req.Scope)
if !client.ValidateScopes(requestedScopes) {
return redirectError(
c,
req.RedirectURI,
"invalid_scope",
"Requested scope not allowed",
req.State,
)
}
// Validate PKCE for public clients
if client.ClientType == "public" && client.RequirePKCE {
if req.CodeChallenge == "" {
return redirectError(
c,
req.RedirectURI,
"invalid_request",
"PKCE required for public clients",
req.State,
)
}
if req.CodeChallengeMethod != PKCEMethodS256 {
return redirectError(
c,
req.RedirectURI,
"invalid_request",
"Only S256 PKCE method supported",
req.State,
)
}
}
// Check authentication
userDID := c.Get("user_did")
if userDID == nil {
// Store authorization request and redirect to authentication
sessionID := generateSessionID()
// TODO: Store auth request in session store
authURL := fmt.Sprintf(
"/auth/login?session_id=%s&return_to=%s",
sessionID,
c.Request().URL.String(),
)
return c.Redirect(http.StatusFound, authURL)
}
// Check consent
if client.RequiresConsent &&
!oauth2Provider.hasValidConsent(userDID.(string), req.ClientID, requestedScopes) {
// Render consent page
return renderOAuth2ConsentPage(c, req, client)
}
// Generate authorization code
code := generateSecureToken(32)
authCode := &OAuth2AuthorizationCode{
Code: code,
ClientID: req.ClientID,
UserDID: userDID.(string),
RedirectURI: req.RedirectURI,
Scopes: requestedScopes,
State: req.State,
Nonce: req.Nonce,
CodeChallenge: req.CodeChallenge,
CodeChallengeMethod: req.CodeChallengeMethod,
ExpiresAt: time.Now().Add(10 * time.Minute),
UCANContext: oauth2Provider.buildUCANContext(userDID.(string)),
}
// Store authorization code
oauth2Provider.authCodeStore.Store(authCode)
// Build redirect URL
redirectURL := buildAuthorizationRedirect(req.RedirectURI, code, req.State)
return c.Redirect(http.StatusFound, redirectURL)
}
// HandleOAuth2Token handles OAuth2 token requests
func HandleOAuth2Token(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
var req OAuth2TokenRequest
if err := c.Bind(&req); err != nil {
return oauth2TokenError(c, "invalid_request", "Invalid token request")
}
// Authenticate client
client, err := oauth2Provider.authenticateClient(c, &req)
if err != nil {
return oauth2TokenError(c, "invalid_client", "Client authentication failed")
}
// Handle grant type
switch req.GrantType {
case "authorization_code":
return oauth2Provider.handleAuthorizationCodeGrant(c, client, &req)
case "refresh_token":
return oauth2Provider.handleRefreshTokenGrant(c, client, &req)
case "client_credentials":
return oauth2Provider.handleClientCredentialsGrant(c, client, &req)
default:
return oauth2TokenError(c, "unsupported_grant_type", "Grant type not supported")
}
}
// HandleOAuth2Introspection handles token introspection requests
func HandleOAuth2Introspection(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
var req OAuth2IntrospectionRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, &OAuth2IntrospectionResponse{Active: false})
}
// Authenticate client
client, err := oauth2Provider.authenticateClient(c, &OAuth2TokenRequest{
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
})
if err != nil {
return c.JSON(http.StatusUnauthorized, &OAuth2IntrospectionResponse{Active: false})
}
// Introspect token
response := oauth2Provider.introspectToken(req.Token, req.TokenTypeHint, client)
return c.JSON(http.StatusOK, response)
}
// HandleOAuth2Revocation handles token revocation requests
func HandleOAuth2Revocation(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
var req OAuth2RevocationRequest
if err := c.Bind(&req); err != nil {
return c.NoContent(http.StatusBadRequest)
}
// Authenticate client
client, err := oauth2Provider.authenticateClient(c, &OAuth2TokenRequest{
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
})
if err != nil {
return c.NoContent(http.StatusUnauthorized)
}
// Revoke token
oauth2Provider.revokeToken(req.Token, req.TokenTypeHint, client)
return c.NoContent(http.StatusOK)
}
// Private methods
func (p *OAuth2Provider) handleAuthorizationCodeGrant(
c echo.Context,
client *OAuth2Client,
req *OAuth2TokenRequest,
) error {
// Retrieve authorization code
authCode := p.authCodeStore.Exchange(req.Code)
if authCode == nil {
return oauth2TokenError(c, "invalid_grant", "Invalid authorization code")
}
// Validate code hasn't expired
if time.Now().After(authCode.ExpiresAt) {
return oauth2TokenError(c, "invalid_grant", "Authorization code expired")
}
// Validate client
if authCode.ClientID != client.ClientID {
return oauth2TokenError(c, "invalid_grant", "Code was issued to different client")
}
// Validate redirect URI
if authCode.RedirectURI != req.RedirectURI {
return oauth2TokenError(c, "invalid_grant", "Redirect URI mismatch")
}
// Validate PKCE if present
if authCode.CodeChallenge != "" {
if !p.validatePKCE(req.CodeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) {
return oauth2TokenError(c, "invalid_grant", "Invalid PKCE verifier")
}
}
// Create UCAN delegation
ucanToken, err := p.ucanDelegator.CreateDelegation(
authCode.UserDID,
client.ClientID,
authCode.Scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return oauth2TokenError(c, "server_error", "Failed to create delegation")
}
// Generate tokens
accessToken := p.generateAccessToken(authCode, ucanToken)
refreshToken := p.generateRefreshToken(authCode)
// Store tokens
p.accessTokenStore.Store(accessToken)
p.refreshTokenStore.Store(refreshToken)
// Generate ID token if openid scope present
var idToken string
if contains(authCode.Scopes, "openid") {
idToken, _ = generateIDToken(authCode.UserDID, client.ClientID, authCode.Nonce)
}
// Return token response
response := &OAuth2TokenResponse{
AccessToken: accessToken.Token,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: refreshToken.Token,
Scope: strings.Join(authCode.Scopes, " "),
IDToken: idToken,
UCANToken: ucanToken.Raw,
}
return c.JSON(http.StatusOK, response)
}
func (p *OAuth2Provider) handleRefreshTokenGrant(
c echo.Context,
client *OAuth2Client,
req *OAuth2TokenRequest,
) error {
// Retrieve refresh token
oldRefreshToken := p.refreshTokenStore.Get(req.RefreshToken)
if oldRefreshToken == nil {
return oauth2TokenError(c, "invalid_grant", "Invalid refresh token")
}
// Validate client
if oldRefreshToken.ClientID != client.ClientID {
return oauth2TokenError(c, "invalid_grant", "Token was issued to different client")
}
// Validate expiration
if time.Now().After(oldRefreshToken.ExpiresAt) {
return oauth2TokenError(c, "invalid_grant", "Refresh token expired")
}
// Rotate refresh token
p.refreshTokenStore.Revoke(req.RefreshToken)
// Create new UCAN delegation
ucanToken, err := p.ucanDelegator.CreateDelegation(
oldRefreshToken.UserDID,
client.ClientID,
oldRefreshToken.Scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return oauth2TokenError(c, "server_error", "Failed to create delegation")
}
// Generate new tokens
newAccessToken := &OAuth2AccessToken{
Token: generateSecureToken(32),
UserDID: oldRefreshToken.UserDID,
ClientID: client.ClientID,
Scopes: oldRefreshToken.Scopes,
ExpiresAt: time.Now().Add(time.Hour),
IssuedAt: time.Now(),
UCANToken: ucanToken,
}
newRefreshToken := &OAuth2RefreshToken{
Token: generateSecureToken(32),
AccessToken: newAccessToken.Token,
ClientID: client.ClientID,
UserDID: oldRefreshToken.UserDID,
Scopes: oldRefreshToken.Scopes,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
IssuedAt: time.Now(),
RotationCount: oldRefreshToken.RotationCount + 1,
}
// Store new tokens
p.accessTokenStore.Store(newAccessToken)
p.refreshTokenStore.Store(newRefreshToken)
// Return response
response := &OAuth2TokenResponse{
AccessToken: newAccessToken.Token,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: newRefreshToken.Token,
Scope: strings.Join(newAccessToken.Scopes, " "),
UCANToken: ucanToken.Raw,
}
return c.JSON(http.StatusOK, response)
}
func (p *OAuth2Provider) handleClientCredentialsGrant(
c echo.Context,
client *OAuth2Client,
req *OAuth2TokenRequest,
) error {
// Client credentials grant is only for confidential clients
if client.ClientType != "confidential" {
return oauth2TokenError(
c,
"unauthorized_client",
"Client type not authorized for this grant",
)
}
// Parse requested scopes
scopes := parseScopes(req.Scope)
if !client.ValidateScopes(scopes) {
return oauth2TokenError(c, "invalid_scope", "Requested scope not allowed")
}
// Create service-to-service UCAN token
ucanToken, err := p.ucanDelegator.CreateServiceDelegation(
client.ClientID,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return oauth2TokenError(c, "server_error", "Failed to create delegation")
}
// Generate access token
accessToken := &OAuth2AccessToken{
Token: generateSecureToken(32),
UserDID: "", // No user for client credentials
ClientID: client.ClientID,
Scopes: scopes,
ExpiresAt: time.Now().Add(time.Hour),
IssuedAt: time.Now(),
UCANToken: ucanToken,
TokenType: "client_credentials",
}
// Store token
p.accessTokenStore.Store(accessToken)
// Return response
response := &OAuth2TokenResponse{
AccessToken: accessToken.Token,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
UCANToken: ucanToken.Raw,
}
return c.JSON(http.StatusOK, response)
}
func (p *OAuth2Provider) authenticateClient(
c echo.Context,
req *OAuth2TokenRequest,
) (*OAuth2Client, error) {
// Try Basic Auth first
if username, password, ok := c.Request().BasicAuth(); ok {
client, err := p.clientRegistry.GetClient(username)
if err != nil {
return nil, err
}
if client.ClientType == "confidential" &&
subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(password)) == 1 {
return client, nil
}
return nil, fmt.Errorf("invalid client credentials")
}
// Try client_secret_post
if req.ClientID != "" && req.ClientSecret != "" {
client, err := p.clientRegistry.GetClient(req.ClientID)
if err != nil {
return nil, err
}
if client.ClientType == "confidential" &&
subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(req.ClientSecret)) == 1 {
return client, nil
}
return nil, fmt.Errorf("invalid client credentials")
}
// Try client_assertion (JWT)
if req.ClientAssertion != "" &&
req.ClientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
// TODO: Implement JWT client assertion validation
return nil, fmt.Errorf("JWT client assertion not yet implemented")
}
// Public client (no authentication)
if req.ClientID != "" {
client, err := p.clientRegistry.GetClient(req.ClientID)
if err != nil {
return nil, err
}
if client.ClientType == "public" {
return client, nil
}
}
return nil, fmt.Errorf("client authentication required")
}
func (p *OAuth2Provider) validatePKCE(verifier, challenge, method string) bool {
if method == "" {
method = PKCEMethodPlain
}
computed := computePKCEChallenge(verifier, method)
return subtle.ConstantTimeCompare([]byte(computed), []byte(challenge)) == 1
}
func (p *OAuth2Provider) hasValidConsent(userDID, clientID string, scopes []string) bool {
p.consentStore.mu.RLock()
defer p.consentStore.mu.RUnlock()
key := fmt.Sprintf("%s:%s", userDID, clientID)
consent, exists := p.consentStore.consents[key]
if !exists {
return false
}
// Check expiration
if time.Now().After(consent.ExpiresAt) {
return false
}
// Check all requested scopes are approved
for _, scope := range scopes {
if !contains(consent.ApprovedScopes, scope) {
return false
}
}
return true
}
func (p *OAuth2Provider) buildUCANContext(userDID string) *UCANAuthContext {
// TODO: Fetch actual vault and DID document data
return &UCANAuthContext{
VaultAddress: fmt.Sprintf("vault_%s", userDID),
EnclaveDataCID: fmt.Sprintf("cid_%s", userDID),
Capabilities: []string{"read", "write", "sign"},
}
}
func (p *OAuth2Provider) generateAccessToken(
authCode *OAuth2AuthorizationCode,
ucanToken *ucan.Token,
) *OAuth2AccessToken {
return &OAuth2AccessToken{
Token: generateSecureToken(32),
UserDID: authCode.UserDID,
ClientID: authCode.ClientID,
Scopes: authCode.Scopes,
ExpiresAt: time.Now().Add(time.Hour),
IssuedAt: time.Now(),
UCANToken: ucanToken,
SessionID: generateSessionID(),
TokenType: "authorization_code",
}
}
func (p *OAuth2Provider) generateRefreshToken(
authCode *OAuth2AuthorizationCode,
) *OAuth2RefreshToken {
return &OAuth2RefreshToken{
Token: generateSecureToken(32),
ClientID: authCode.ClientID,
UserDID: authCode.UserDID,
Scopes: authCode.Scopes,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
IssuedAt: time.Now(),
RotationCount: 0,
}
}
func (p *OAuth2Provider) introspectToken(
token, tokenTypeHint string,
client *OAuth2Client,
) *OAuth2IntrospectionResponse {
// Try access token first
if accessToken := p.accessTokenStore.Get(token); accessToken != nil {
if accessToken.ClientID != client.ClientID {
return &OAuth2IntrospectionResponse{Active: false}
}
return &OAuth2IntrospectionResponse{
Active: time.Now().Before(accessToken.ExpiresAt),
Scope: strings.Join(accessToken.Scopes, " "),
ClientID: accessToken.ClientID,
Username: accessToken.UserDID,
TokenType: "Bearer",
ExpiresAt: accessToken.ExpiresAt.Unix(),
IssuedAt: accessToken.IssuedAt.Unix(),
Subject: accessToken.UserDID,
UCANToken: accessToken.UCANToken.Raw,
}
}
// Try refresh token
if refreshToken := p.refreshTokenStore.Get(token); refreshToken != nil {
if refreshToken.ClientID != client.ClientID {
return &OAuth2IntrospectionResponse{Active: false}
}
return &OAuth2IntrospectionResponse{
Active: time.Now().Before(refreshToken.ExpiresAt),
Scope: strings.Join(refreshToken.Scopes, " "),
ClientID: refreshToken.ClientID,
Username: refreshToken.UserDID,
TokenType: "refresh_token",
ExpiresAt: refreshToken.ExpiresAt.Unix(),
IssuedAt: refreshToken.IssuedAt.Unix(),
Subject: refreshToken.UserDID,
}
}
return &OAuth2IntrospectionResponse{Active: false}
}
func (p *OAuth2Provider) revokeToken(token, tokenTypeHint string, client *OAuth2Client) {
// Try to revoke as access token
if p.accessTokenStore.Revoke(token) {
return
}
// Try to revoke as refresh token
p.refreshTokenStore.Revoke(token)
}
func (p *OAuth2Provider) cleanupExpiredTokens() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
// Cleanup expired authorization codes
p.authCodeStore.CleanupExpired()
// Cleanup expired access tokens
p.accessTokenStore.CleanupExpired()
// Cleanup expired refresh tokens
p.refreshTokenStore.CleanupExpired()
}
}
// Store methods for token stores
func (s *AuthCodeStore) Store(code *OAuth2AuthorizationCode) {
s.mu.Lock()
defer s.mu.Unlock()
s.codes[code.Code] = code
}
func (s *AuthCodeStore) Exchange(code string) *OAuth2AuthorizationCode {
s.mu.Lock()
defer s.mu.Unlock()
authCode, exists := s.codes[code]
if !exists || authCode.Used {
return nil
}
authCode.Used = true
return authCode
}
func (s *AuthCodeStore) CleanupExpired() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for code, authCode := range s.codes {
if now.After(authCode.ExpiresAt) {
delete(s.codes, code)
}
}
}
func (s *AccessTokenStore) Store(token *OAuth2AccessToken) {
s.mu.Lock()
defer s.mu.Unlock()
s.tokens[token.Token] = token
}
func (s *AccessTokenStore) Get(token string) *OAuth2AccessToken {
s.mu.RLock()
defer s.mu.RUnlock()
return s.tokens[token]
}
func (s *AccessTokenStore) Revoke(token string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tokens[token]; exists {
delete(s.tokens, token)
return true
}
return false
}
func (s *AccessTokenStore) CleanupExpired() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for token, accessToken := range s.tokens {
if now.After(accessToken.ExpiresAt) {
delete(s.tokens, token)
}
}
}
func (s *RefreshTokenStore) Store(token *OAuth2RefreshToken) {
s.mu.Lock()
defer s.mu.Unlock()
s.tokens[token.Token] = token
}
func (s *RefreshTokenStore) Get(token string) *OAuth2RefreshToken {
s.mu.RLock()
defer s.mu.RUnlock()
return s.tokens[token]
}
func (s *RefreshTokenStore) Revoke(token string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tokens[token]; exists {
delete(s.tokens, token)
return true
}
return false
}
func (s *RefreshTokenStore) CleanupExpired() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for token, refreshToken := range s.tokens {
if now.After(refreshToken.ExpiresAt) {
delete(s.tokens, token)
}
}
}
// Helper functions
func generateSecureToken(bytes int) string {
b := make([]byte, bytes)
if _, err := rand.Read(b); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(b)
}
func parseScopes(scope string) []string {
if scope == "" {
return []string{}
}
return strings.Split(scope, " ")
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
func isValidResponseType(responseType string) bool {
validTypes := []string{
"code",
"token",
"id_token",
"code id_token",
"code token",
"id_token token",
"code id_token token",
}
return contains(validTypes, responseType)
}
func oauth2Error(c echo.Context, error, description, state string) error {
return c.JSON(http.StatusBadRequest, &OAuth2ErrorResponse{
Error: error,
ErrorDescription: description,
State: state,
})
}
func oauth2TokenError(c echo.Context, error, description string) error {
return c.JSON(http.StatusBadRequest, &OAuth2ErrorResponse{
Error: error,
ErrorDescription: description,
})
}
func redirectError(c echo.Context, redirectURI, error, description, state string) error {
url := fmt.Sprintf("%s?error=%s&error_description=%s&state=%s",
redirectURI, error, description, state)
return c.Redirect(http.StatusFound, url)
}
func buildAuthorizationRedirect(redirectURI, code, state string) string {
if strings.Contains(redirectURI, "?") {
return fmt.Sprintf("%s&code=%s&state=%s", redirectURI, code, state)
}
return fmt.Sprintf("%s?code=%s&state=%s", redirectURI, code, state)
}
func renderOAuth2ConsentPage(
c echo.Context,
req *OAuth2AuthorizationRequest,
client *OAuth2Client,
) error {
// TODO: Render actual consent page
return c.JSON(http.StatusOK, map[string]any{
"client": client,
"scopes": parseScopes(req.Scope),
"state": req.State,
"client_id": req.ClientID,
})
}
func getDefaultOAuth2Config() *OAuth2Config {
baseURL := "https://localhost:8080"
return &OAuth2Config{
Issuer: baseURL,
AuthorizationEndpoint: baseURL + "/oauth2/authorize",
TokenEndpoint: baseURL + "/oauth2/token",
UserInfoEndpoint: baseURL + "/oauth2/userinfo",
JWKSEndpoint: baseURL + "/oauth2/jwks",
RegistrationEndpoint: baseURL + "/oauth2/register",
IntrospectionEndpoint: baseURL + "/oauth2/introspect",
RevocationEndpoint: baseURL + "/oauth2/revoke",
ScopesSupported: []string{
"openid", "profile", "email", "offline_access",
"vault:read", "vault:write", "vault:sign", "vault:admin",
"service:manage", "did:read", "did:write",
},
ResponseTypesSupported: []string{
"code", "token", "id_token",
"code id_token", "code token",
"id_token token", "code id_token token",
},
ResponseModesSupported: []string{
"query", "fragment", "form_post",
},
GrantTypesSupported: []string{
"authorization_code", "implicit", "refresh_token",
"client_credentials", "urn:ietf:params:oauth:grant-type:device_code",
},
SubjectTypesSupported: []string{
"public", "pairwise",
},
IDTokenSigningAlgValuesSupported: []string{
"ES256", "RS256", "HS256",
},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_basic", "client_secret_post",
"client_secret_jwt", "private_key_jwt", "none",
},
ClaimsSupported: []string{
"sub", "iss", "aud", "exp", "iat", "auth_time", "nonce",
"name", "given_name", "family_name", "middle_name", "nickname",
"preferred_username", "profile", "picture", "website", "email",
"email_verified", "did", "vault_id", "ucan_capabilities",
},
CodeChallengeMethodsSupported: []string{
PKCEMethodS256, PKCEMethodPlain,
},
ServiceDocumentation: baseURL + "/docs/oauth2",
UILocalesSupported: []string{"en-US"},
UCANSupported: true,
}
}
-563
View File
@@ -1,563 +0,0 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// RefreshTokenHandler handles OAuth2 refresh token flows with UCAN chains
type RefreshTokenHandler struct {
delegator *UCANDelegator
signer *BlockchainUCANSigner
tokenStore TokenStore
clientStore ClientStore
}
// RefreshTokenRequest represents an OAuth2 refresh token request
type RefreshTokenRequest struct {
GrantType string `json:"grant_type"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
}
// RefreshTokenResponse represents an OAuth2 refresh token response
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
UCANToken string `json:"ucan_token,omitempty"`
}
// UCANRefreshMetadata stores metadata for UCAN refresh chains
type UCANRefreshMetadata struct {
OriginalIssuer string `json:"original_issuer"`
DelegationChain []string `json:"delegation_chain"`
RefreshCount int `json:"refresh_count"`
MaxRefreshCount int `json:"max_refresh_count"`
CreatedAt time.Time `json:"created_at"`
LastRefreshedAt time.Time `json:"last_refreshed_at"`
AttenuationPath []Attenuation `json:"attenuation_path"`
}
// Attenuation represents scope reduction in the delegation chain
type Attenuation struct {
FromScopes []string `json:"from_scopes"`
ToScopes []string `json:"to_scopes"`
Timestamp time.Time `json:"timestamp"`
Reason string `json:"reason,omitempty"`
}
// NewRefreshTokenHandler creates a new refresh token handler
func NewRefreshTokenHandler(
delegator *UCANDelegator,
signer *BlockchainUCANSigner,
tokenStore TokenStore,
clientStore ClientStore,
) *RefreshTokenHandler {
return &RefreshTokenHandler{
delegator: delegator,
signer: signer,
tokenStore: tokenStore,
clientStore: clientStore,
}
}
// HandleRefreshToken handles OAuth2 refresh token requests
func (h *RefreshTokenHandler) HandleRefreshToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse request (handle both JSON and form-encoded)
var req RefreshTokenRequest
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.sendError(w, "invalid_request", "Failed to parse JSON request body")
return
}
} else {
// Parse form data
if err := r.ParseForm(); err != nil {
h.sendError(w, "invalid_request", "Failed to parse form data")
return
}
req.GrantType = r.FormValue("grant_type")
req.RefreshToken = r.FormValue("refresh_token")
req.Scope = r.FormValue("scope")
req.ClientID = r.FormValue("client_id")
req.ClientSecret = r.FormValue("client_secret")
}
// Validate grant type
if req.GrantType != "refresh_token" {
h.sendError(w, "unsupported_grant_type", "Only refresh_token grant type is supported")
return
}
// Validate refresh token
if req.RefreshToken == "" {
h.sendError(w, "invalid_request", "Missing refresh_token parameter")
return
}
// Authenticate client
clientID, clientSecret := h.extractClientCredentials(r, &req)
ctx := r.Context()
if err := h.clientStore.ValidateClientCredentials(ctx, clientID, clientSecret); err != nil {
h.sendError(w, "invalid_client", "Client authentication failed")
return
}
// Get client information
client, err := h.clientStore.GetClient(ctx, clientID)
if err != nil {
h.sendError(w, "invalid_client", "Client not found")
return
}
// Process refresh token
response, err := h.processRefreshToken(ctx, &req, client)
if err != nil {
h.sendError(w, "invalid_grant", err.Error())
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
json.NewEncoder(w).Encode(response)
}
// processRefreshToken processes the refresh token and returns new tokens
func (h *RefreshTokenHandler) processRefreshToken(
ctx context.Context,
req *RefreshTokenRequest,
client *OAuth2Client,
) (*RefreshTokenResponse, error) {
// Retrieve stored refresh token
storedToken, err := h.tokenStore.GetToken(ctx, req.RefreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
// Validate token type
if storedToken.TokenType != "refresh_token" {
return nil, fmt.Errorf("token is not a refresh token")
}
// Validate client binding
if storedToken.ClientID != client.ClientID {
return nil, fmt.Errorf("refresh token was issued to a different client")
}
// Check if refresh token has expired
if time.Now().After(storedToken.ExpiresAt) {
return nil, fmt.Errorf("refresh token has expired")
}
// Get refresh metadata
metadata, err := h.getRefreshMetadata(ctx, req.RefreshToken)
if err != nil {
// Initialize metadata for first refresh
metadata = &UCANRefreshMetadata{
OriginalIssuer: storedToken.UserDID,
DelegationChain: []string{},
RefreshCount: 0,
MaxRefreshCount: 10, // Default max refresh count
CreatedAt: time.Now(),
AttenuationPath: []Attenuation{},
}
}
// Check refresh count limit
if metadata.RefreshCount >= metadata.MaxRefreshCount {
return nil, fmt.Errorf("refresh token has reached maximum refresh count")
}
// Parse requested scopes
requestedScopes := storedToken.Scopes
if req.Scope != "" {
requestedScopes = strings.Split(req.Scope, " ")
// Validate scope reduction (attenuate permissions)
if !h.validateScopeAttenuation(requestedScopes, storedToken.Scopes) {
return nil, fmt.Errorf("requested scopes exceed refresh token scopes")
}
// Record attenuation
if !h.scopesEqual(requestedScopes, storedToken.Scopes) {
metadata.AttenuationPath = append(metadata.AttenuationPath, Attenuation{
FromScopes: storedToken.Scopes,
ToScopes: requestedScopes,
Timestamp: time.Now(),
Reason: "Client requested scope reduction",
})
}
}
// Create new UCAN token with delegation chain
newUCANToken, err := h.createRefreshedUCANToken(
ctx,
metadata,
storedToken,
client,
requestedScopes,
)
if err != nil {
return nil, fmt.Errorf("failed to create refreshed UCAN token: %w", err)
}
// Generate new access token
accessTokenID := h.generateTokenID()
accessToken := &StoredToken{
TokenID: accessTokenID,
TokenType: "access_token",
AccessToken: accessTokenID,
ExpiresAt: time.Now().Add(time.Hour),
Scopes: requestedScopes,
ClientID: client.ClientID,
UserDID: storedToken.UserDID,
UCANToken: newUCANToken,
}
if err := h.tokenStore.StoreToken(ctx, accessToken); err != nil {
return nil, fmt.Errorf("failed to store new access token: %w", err)
}
// Update refresh metadata
metadata.RefreshCount++
metadata.LastRefreshedAt = time.Now()
metadata.DelegationChain = append(metadata.DelegationChain, newUCANToken)
// Optionally rotate refresh token
newRefreshTokenID := ""
if h.shouldRotateRefreshToken(metadata) {
newRefreshTokenID = h.generateTokenID()
newRefreshToken := &StoredToken{
TokenID: newRefreshTokenID,
TokenType: "refresh_token",
RefreshToken: newRefreshTokenID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
Scopes: requestedScopes,
ClientID: client.ClientID,
UserDID: storedToken.UserDID,
}
if err := h.tokenStore.StoreToken(ctx, newRefreshToken); err != nil {
// Non-fatal, continue with existing refresh token
newRefreshTokenID = ""
} else {
// Revoke old refresh token
h.tokenStore.RevokeToken(ctx, req.RefreshToken)
// Store metadata for new refresh token
h.storeRefreshMetadata(ctx, newRefreshTokenID, metadata)
}
} else {
// Update metadata for existing refresh token
h.storeRefreshMetadata(ctx, req.RefreshToken, metadata)
}
// Build response
response := &RefreshTokenResponse{
AccessToken: accessTokenID,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(requestedScopes, " "),
UCANToken: newUCANToken,
}
if newRefreshTokenID != "" {
response.RefreshToken = newRefreshTokenID
}
return response, nil
}
// createRefreshedUCANToken creates a new UCAN token with proper delegation chain
func (h *RefreshTokenHandler) createRefreshedUCANToken(
ctx context.Context,
metadata *UCANRefreshMetadata,
storedToken *StoredToken,
client *OAuth2Client,
scopes []string,
) (string, error) {
// Build proof chain from previous delegations
proofs := make([]ucan.Proof, 0, len(metadata.DelegationChain))
for _, tokenStr := range metadata.DelegationChain {
proofs = append(proofs, ucan.Proof(tokenStr))
}
// Add original token as proof if exists
if storedToken.UCANToken != "" {
proofs = append([]ucan.Proof{ucan.Proof(storedToken.UCANToken)}, proofs...)
}
// Determine issuer and audience
issuer := metadata.OriginalIssuer
if issuer == "" {
issuer = storedToken.UserDID
}
audience := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
audience = did
}
// Create resource context with refresh metadata
resourceContext := map[string]string{
"refresh_count": fmt.Sprintf("%d", metadata.RefreshCount),
"original_issuer": metadata.OriginalIssuer,
"delegation_type": "refresh_token",
"client_id": client.ClientID,
}
// Map OAuth scopes to UCAN attenuations
attenuations := h.delegator.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
// Create UCAN token with delegation chain
ucanToken := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(time.Hour).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: h.createRefreshFact(metadata, scopes),
},
},
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return "", fmt.Errorf("failed to sign UCAN token: %w", err)
}
// Validate the delegation chain
if len(metadata.DelegationChain) > 0 {
allTokens := append([]string{storedToken.UCANToken}, metadata.DelegationChain...)
allTokens = append(allTokens, signedToken)
if err := h.signer.ValidateDelegationChain(allTokens); err != nil {
return "", fmt.Errorf("invalid delegation chain: %w", err)
}
}
return signedToken, nil
}
// validateScopeAttenuation validates that requested scopes are properly attenuated
func (h *RefreshTokenHandler) validateScopeAttenuation(requested, allowed []string) bool {
// Build allowed scope map
allowedMap := make(map[string]bool)
for _, scope := range allowed {
allowedMap[scope] = true
}
// Check each requested scope
for _, scope := range requested {
if !allowedMap[scope] {
// Check if a parent scope allows this
found := false
for _, allowedScope := range allowed {
if h.delegator.scopeMapper.IsHierarchicalScope(allowedScope, scope) {
found = true
break
}
}
if !found {
return false
}
}
}
return true
}
// shouldRotateRefreshToken determines if refresh token should be rotated
func (h *RefreshTokenHandler) shouldRotateRefreshToken(metadata *UCANRefreshMetadata) bool {
// Rotate on every use for maximum security
// Could be configured based on policy
return true
}
// extractClientCredentials extracts client credentials from request
func (h *RefreshTokenHandler) extractClientCredentials(
r *http.Request,
req *RefreshTokenRequest,
) (string, string) {
// Try Basic Auth first
if clientID, clientSecret, ok := r.BasicAuth(); ok {
return clientID, clientSecret
}
// Fall back to request body
return req.ClientID, req.ClientSecret
}
// getRefreshMetadata retrieves refresh metadata from storage
func (h *RefreshTokenHandler) getRefreshMetadata(
ctx context.Context,
refreshTokenID string,
) (*UCANRefreshMetadata, error) {
// In production, this would retrieve from persistent storage
// For now, return error to initialize new metadata
return nil, fmt.Errorf("metadata not found")
}
// storeRefreshMetadata stores refresh metadata
func (h *RefreshTokenHandler) storeRefreshMetadata(
ctx context.Context,
refreshTokenID string,
metadata *UCANRefreshMetadata,
) error {
// In production, this would persist to storage
// For now, just return success
return nil
}
// createRefreshFact creates a fact for refresh token
func (h *RefreshTokenHandler) createRefreshFact(
metadata *UCANRefreshMetadata,
scopes []string,
) json.RawMessage {
fact := map[string]any{
"type": "refresh_token",
"refresh_count": metadata.RefreshCount,
"original_issuer": metadata.OriginalIssuer,
"scopes": scopes,
"refreshed_at": time.Now().Unix(),
"delegation_length": len(metadata.DelegationChain),
}
// Add attenuation info if present
if len(metadata.AttenuationPath) > 0 {
fact["attenuations"] = len(metadata.AttenuationPath)
lastAttenuation := metadata.AttenuationPath[len(metadata.AttenuationPath)-1]
fact["last_attenuation"] = map[string]any{
"from": strings.Join(lastAttenuation.FromScopes, " "),
"to": strings.Join(lastAttenuation.ToScopes, " "),
"at": lastAttenuation.Timestamp.Unix(),
}
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
// scopesEqual checks if two scope slices are equal
func (h *RefreshTokenHandler) scopesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
aMap := make(map[string]bool)
for _, scope := range a {
aMap[scope] = true
}
for _, scope := range b {
if !aMap[scope] {
return false
}
}
return true
}
// generateTokenID generates a unique token identifier
func (h *RefreshTokenHandler) generateTokenID() string {
// In production, use a proper UUID or secure random generator
return fmt.Sprintf("tok_%d_%s", time.Now().UnixNano(), h.randomString(16))
}
// randomString generates a random string
func (h *RefreshTokenHandler) randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(result)
}
// sendError sends an OAuth error response
func (h *RefreshTokenHandler) sendError(w http.ResponseWriter, errorCode, errorDescription string) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
w.WriteHeader(http.StatusBadRequest)
response := map[string]string{
"error": errorCode,
"error_description": errorDescription,
}
json.NewEncoder(w).Encode(response)
}
// HandleUCANRefresh handles UCAN-specific refresh requests
func (h *RefreshTokenHandler) HandleUCANRefresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse UCAN token from Authorization header
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
h.sendError(w, "invalid_request", "Missing or invalid Authorization header")
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// Verify the UCAN token
ucanToken, err := h.signer.VerifySignature(tokenString)
if err != nil {
h.sendError(w, "invalid_grant", "Invalid UCAN token")
return
}
// Check if token can be refreshed (not expired beyond grace period)
gracePeriod := int64(300) // 5 minutes grace period
if time.Now().Unix() > ucanToken.ExpiresAt+gracePeriod {
h.sendError(w, "invalid_grant", "Token expired beyond grace period")
return
}
// Create refreshed token with extended expiration
newToken, err := h.signer.RefreshToken(tokenString, time.Hour)
if err != nil {
h.sendError(w, "server_error", "Failed to refresh token")
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(map[string]any{
"ucan_token": newToken,
"token_type": "UCAN",
"expires_in": 3600,
})
}
-383
View File
@@ -1,383 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
)
// DynamicClientRegistrationRequest represents a client registration request per RFC 7591
type DynamicClientRegistrationRequest struct {
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ApplicationType string `json:"application_type,omitempty"`
Contacts []string `json:"contacts,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TosURI string `json:"tos_uri,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"`
SoftwareID string `json:"software_id,omitempty"`
SoftwareVersion string `json:"software_version,omitempty"`
}
// DynamicClientRegistrationResponse represents the response for client registration
type DynamicClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
Scope string `json:"scope"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
ApplicationType string `json:"application_type"`
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TosURI string `json:"tos_uri,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"`
}
// HandleDynamicClientRegistration handles dynamic client registration per RFC 7591
func (s *OAuth2Provider) HandleDynamicClientRegistration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse registration request
var req DynamicClientRegistrationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid registration request", http.StatusBadRequest)
return
}
// Validate required fields
if req.ClientName == "" {
http.Error(w, "client_name is required", http.StatusBadRequest)
return
}
if len(req.RedirectURIs) == 0 {
http.Error(w, "redirect_uris is required", http.StatusBadRequest)
return
}
// Validate redirect URIs
for _, uri := range req.RedirectURIs {
if !isValidRedirectURI(uri) {
http.Error(w, "Invalid redirect URI: "+uri, http.StatusBadRequest)
return
}
}
// Set defaults if not provided
if len(req.GrantTypes) == 0 {
req.GrantTypes = []string{"authorization_code"}
}
if len(req.ResponseTypes) == 0 {
req.ResponseTypes = []string{"code"}
}
if req.TokenEndpointAuthMethod == "" {
// Default based on application type
if req.ApplicationType == "native" || req.ApplicationType == "browser" {
req.TokenEndpointAuthMethod = "none" // Public client
} else {
req.TokenEndpointAuthMethod = "client_secret_basic"
}
}
if req.ApplicationType == "" {
req.ApplicationType = "web"
}
// Validate grant types and response types
if !validateGrantTypes(req.GrantTypes) {
http.Error(w, "Invalid grant types", http.StatusBadRequest)
return
}
if !validateResponseTypes(req.ResponseTypes) {
http.Error(w, "Invalid response types", http.StatusBadRequest)
return
}
// Validate scopes if scope mapper is available
if req.Scope != "" && s.scopeMapper != nil {
scopes := strings.Split(req.Scope, " ")
for _, scope := range scopes {
// Check if scope is valid using the scope mapper
if _, exists := s.scopeMapper.GetScope(scope); !exists {
http.Error(w, "Invalid scope: "+scope, http.StatusBadRequest)
return
}
}
}
// Generate client credentials
clientID := generateDynamicClientID()
var clientSecret string
var clientSecretExpiresAt int64
// Only generate secret for confidential clients
if req.TokenEndpointAuthMethod != "none" {
clientSecret = generateDynamicClientSecret()
// Client secrets expire in 1 year by default
clientSecretExpiresAt = time.Now().Add(365 * 24 * time.Hour).Unix()
}
// Create OAuth2 client
client := &OAuth2Client{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURIs: req.RedirectURIs,
AllowedScopes: strings.Split(req.Scope, " "),
AllowedGrants: req.GrantTypes,
TokenLifetime: time.Hour, // Default 1 hour
RequirePKCE: false,
TrustedClient: false,
RequiresConsent: true,
Metadata: map[string]string{
"client_name": req.ClientName,
"application_type": req.ApplicationType,
"logo_uri": req.LogoURI,
"client_uri": req.ClientURI,
"policy_uri": req.PolicyURI,
"tos_uri": req.TosURI,
"jwks_uri": req.JwksURI,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Set client type based on auth method
if req.TokenEndpointAuthMethod == "none" {
client.ClientType = "public"
} else {
client.ClientType = "confidential"
}
// Determine if client requires PKCE
if req.ApplicationType == "native" || req.ApplicationType == "browser" {
client.RequirePKCE = true
}
// Store client in the registry
if s.clientRegistry != nil {
if err := s.clientRegistry.RegisterClient(client); err != nil {
http.Error(w, "Failed to register client", http.StatusInternalServerError)
return
}
}
// Build response
resp := DynamicClientRegistrationResponse{
ClientID: clientID,
ClientSecret: clientSecret,
ClientName: req.ClientName,
RedirectURIs: req.RedirectURIs,
GrantTypes: req.GrantTypes,
ResponseTypes: req.ResponseTypes,
Scope: req.Scope,
TokenEndpointAuthMethod: req.TokenEndpointAuthMethod,
ApplicationType: req.ApplicationType,
ClientIDIssuedAt: time.Now().Unix(),
ClientSecretExpiresAt: clientSecretExpiresAt,
LogoURI: req.LogoURI,
ClientURI: req.ClientURI,
PolicyURI: req.PolicyURI,
TosURI: req.TosURI,
JwksURI: req.JwksURI,
}
// Return registration response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
// HandleClientConfiguration handles client configuration retrieval
func (s *OAuth2Provider) HandleClientConfiguration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract client ID from path or query
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
// Try to extract from path (e.g., /register/{client_id})
parts := strings.Split(r.URL.Path, "/")
if len(parts) > 2 {
clientID = parts[len(parts)-1]
}
}
if clientID == "" {
http.Error(w, "client_id is required", http.StatusBadRequest)
return
}
// Validate access token for client management
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", `Bearer realm="client_configuration"`)
http.Error(w, "Access token required", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
// Validate token in the access token store
if s.accessTokenStore == nil {
http.Error(w, "Token store not available", http.StatusInternalServerError)
return
}
// Check token validity (simplified for now)
// In production, this should validate the token properly
if token == "" {
http.Error(w, "Invalid or expired access token", http.StatusUnauthorized)
return
}
// Get client from registry
if s.clientRegistry == nil {
http.Error(w, "Client registry not available", http.StatusInternalServerError)
return
}
client, err := s.clientRegistry.GetClient(clientID)
if err != nil {
http.Error(w, "Client not found", http.StatusNotFound)
return
}
// Build response
resp := DynamicClientRegistrationResponse{
ClientID: client.ClientID,
ClientName: client.Metadata["client_name"],
RedirectURIs: client.RedirectURIs,
GrantTypes: client.AllowedGrants,
ResponseTypes: []string{"code", "token"}, // Default response types
Scope: strings.Join(client.AllowedScopes, " "),
TokenEndpointAuthMethod: getTokenEndpointAuthMethod(client),
ApplicationType: client.Metadata["application_type"],
ClientIDIssuedAt: client.CreatedAt.Unix(),
LogoURI: client.Metadata["logo_uri"],
ClientURI: client.Metadata["client_uri"],
PolicyURI: client.Metadata["policy_uri"],
TosURI: client.Metadata["tos_uri"],
JwksURI: client.Metadata["jwks_uri"],
}
// Return client configuration
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(resp)
}
// getTokenEndpointAuthMethod determines the auth method from client type
func getTokenEndpointAuthMethod(client *OAuth2Client) string {
if client.ClientType == "public" {
return "none"
}
return "client_secret_basic"
}
// Helper functions for dynamic registration
func generateDynamicClientID() string {
// Generate a random client ID for dynamic registration
b := make([]byte, 16)
_, _ = rand.Read(b)
return "dyn_client_" + base64.RawURLEncoding.EncodeToString(b)
}
func generateDynamicClientSecret() string {
// Generate a secure random secret for dynamic registration
b := make([]byte, 32)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func isValidRedirectURI(uri string) bool {
// Basic validation - in production, this should be more comprehensive
if uri == "" {
return false
}
// Allow localhost for development
if strings.HasPrefix(uri, "http://localhost") || strings.HasPrefix(uri, "http://127.0.0.1") {
return true
}
// Require HTTPS for production URIs
if !strings.HasPrefix(uri, "https://") {
// Allow custom schemes for native apps
if strings.Contains(uri, "://") {
return true
}
return false
}
return true
}
func validateGrantTypes(grantTypes []string) bool {
validGrants := map[string]bool{
"authorization_code": true,
"implicit": true,
"refresh_token": true,
"client_credentials": true,
"password": true,
}
for _, grant := range grantTypes {
if !validGrants[grant] {
return false
}
}
return true
}
func validateResponseTypes(responseTypes []string) bool {
validTypes := map[string]bool{
"code": true,
"token": true,
"id_token": true,
}
for _, respType := range responseTypes {
// Handle composite types like "code id_token"
parts := strings.Split(respType, " ")
for _, part := range parts {
if !validTypes[part] {
return false
}
}
}
return true
}
func hasScope(scopes []string, requiredScope string) bool {
for _, scope := range scopes {
if scope == requiredScope {
return true
}
}
return false
}
-412
View File
@@ -1,412 +0,0 @@
package handlers
import (
"fmt"
"strings"
"github.com/sonr-io/sonr/crypto/ucan"
)
// ScopeMapper manages OAuth scope to UCAN capability mapping
type ScopeMapper struct {
scopeDefinitions map[string]*OAuth2ScopeDefinition
ucanTemplates map[string]*ucan.Attenuation
}
// NewScopeMapper creates a new scope mapper with standard scopes
func NewScopeMapper() *ScopeMapper {
mapper := &ScopeMapper{
scopeDefinitions: make(map[string]*OAuth2ScopeDefinition),
ucanTemplates: make(map[string]*ucan.Attenuation),
}
mapper.initializeStandardScopes()
return mapper
}
// initializeStandardScopes defines the standard OAuth scopes and their UCAN mappings
func (m *ScopeMapper) initializeStandardScopes() {
// OpenID Connect scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "openid",
Description: "OpenID Connect authentication",
UCANActions: []string{"authenticate"},
ResourceType: "identity",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "profile",
Description: "Access to user profile information",
UCANActions: []string{"read"},
ResourceType: "did",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "email",
Description: "Access to user email",
UCANActions: []string{"read"},
ResourceType: "contact",
RequiresAuth: true,
Sensitive: true,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "offline_access",
Description: "Maintain access when user is not present",
UCANActions: []string{"refresh"},
ResourceType: "session",
RequiresAuth: true,
Sensitive: true,
})
// Vault scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:read",
Description: "Read access to vault data",
UCANActions: []string{"read"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: false,
ParentScope: "",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:write",
Description: "Write access to vault data",
UCANActions: []string{"read", "write"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: true,
ParentScope: "vault:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:sign",
Description: "Signing operations with vault keys",
UCANActions: []string{"read", "sign"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: true,
ParentScope: "vault:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:admin",
Description: "Full administrative access to vault",
UCANActions: []string{"read", "write", "sign", "export", "import", "delete", "admin"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: true,
ParentScope: "vault:write",
ChildScopes: []string{"vault:read", "vault:write", "vault:sign"},
})
// Service scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "service:read",
Description: "Read service information",
UCANActions: []string{"read"},
ResourceType: "service",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "service:write",
Description: "Create and update services",
UCANActions: []string{"read", "write"},
ResourceType: "service",
RequiresAuth: true,
Sensitive: true,
ParentScope: "service:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "service:manage",
Description: "Full service management capabilities",
UCANActions: []string{"read", "write", "delete", "admin"},
ResourceType: "service",
RequiresAuth: true,
Sensitive: true,
ParentScope: "service:write",
ChildScopes: []string{"service:read", "service:write"},
})
// DID scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "did:read",
Description: "Read DID documents",
UCANActions: []string{"read"},
ResourceType: "did",
RequiresAuth: false,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "did:write",
Description: "Update DID documents",
UCANActions: []string{"read", "write"},
ResourceType: "did",
RequiresAuth: true,
Sensitive: true,
ParentScope: "did:read",
})
// DWN (Decentralized Web Node) scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "dwn:read",
Description: "Read data from DWN",
UCANActions: []string{"read"},
ResourceType: "dwn",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "dwn:write",
Description: "Write data to DWN",
UCANActions: []string{"read", "write"},
ResourceType: "dwn",
RequiresAuth: true,
Sensitive: true,
ParentScope: "dwn:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "dwn:admin",
Description: "Full administrative access to DWN",
UCANActions: []string{"read", "write", "admin", "protocols-configure"},
ResourceType: "dwn",
RequiresAuth: true,
Sensitive: true,
ParentScope: "dwn:write",
ChildScopes: []string{"dwn:read", "dwn:write"},
})
}
// RegisterScope registers a new scope definition
func (m *ScopeMapper) RegisterScope(scope *OAuth2ScopeDefinition) error {
if scope.Name == "" {
return fmt.Errorf("scope name is required")
}
m.scopeDefinitions[scope.Name] = scope
// Create UCAN template for this scope
m.createUCANTemplate(scope)
return nil
}
// GetScope retrieves a scope definition
func (m *ScopeMapper) GetScope(name string) (*OAuth2ScopeDefinition, bool) {
scope, exists := m.scopeDefinitions[name]
return scope, exists
}
// MapToUCAN maps OAuth scopes to UCAN attenuations
func (m *ScopeMapper) MapToUCAN(
scopes []string,
userDID string,
clientID string,
resourceContext map[string]string,
) []ucan.Attenuation {
attenuations := []ucan.Attenuation{}
for _, scopeName := range scopes {
scope, exists := m.scopeDefinitions[scopeName]
if !exists {
continue
}
// Create attenuation for this scope
attenuation := m.createAttenuation(scope, userDID, clientID, resourceContext)
attenuations = append(attenuations, attenuation)
// Add child scope attenuations if this is a parent scope
for _, childScope := range scope.ChildScopes {
if childDef, exists := m.scopeDefinitions[childScope]; exists {
childAttenuation := m.createAttenuation(
childDef,
userDID,
clientID,
resourceContext,
)
attenuations = append(attenuations, childAttenuation)
}
}
}
return attenuations
}
// ValidateScopes validates that the requested scopes are valid
func (m *ScopeMapper) ValidateScopes(scopes []string) error {
for _, scope := range scopes {
if _, exists := m.scopeDefinitions[scope]; !exists {
return fmt.Errorf("invalid scope: %s", scope)
}
}
return nil
}
// GetScopeDescriptions returns human-readable descriptions for scopes
func (m *ScopeMapper) GetScopeDescriptions(scopes []string) map[string]string {
descriptions := make(map[string]string)
for _, scope := range scopes {
if def, exists := m.scopeDefinitions[scope]; exists {
descriptions[scope] = def.Description
}
}
return descriptions
}
// GetSensitiveScopes returns only the sensitive scopes from a list
func (m *ScopeMapper) GetSensitiveScopes(scopes []string) []string {
sensitive := []string{}
for _, scope := range scopes {
if def, exists := m.scopeDefinitions[scope]; exists && def.Sensitive {
sensitive = append(sensitive, scope)
}
}
return sensitive
}
// IsHierarchicalScope checks if one scope includes another
func (m *ScopeMapper) IsHierarchicalScope(parentScope, childScope string) bool {
parent, exists := m.scopeDefinitions[parentScope]
if !exists {
return false
}
// Check direct children
for _, child := range parent.ChildScopes {
if child == childScope {
return true
}
// Recursive check
if m.IsHierarchicalScope(child, childScope) {
return true
}
}
return false
}
// Private helper methods
func (m *ScopeMapper) createUCANTemplate(scope *OAuth2ScopeDefinition) {
// Create a template attenuation for this scope
capability := &ucan.SimpleCapability{
Action: strings.Join(scope.UCANActions, ","),
}
resource := &SimpleResource{
Scheme: scope.ResourceType,
Value: "{resource_id}",
}
m.ucanTemplates[scope.Name] = &ucan.Attenuation{
Capability: capability,
Resource: resource,
}
}
func (m *ScopeMapper) createAttenuation(
scope *OAuth2ScopeDefinition,
userDID string,
clientID string,
resourceContext map[string]string,
) ucan.Attenuation {
// Create capability based on scope actions
var capability ucan.Capability
if len(scope.UCANActions) == 1 {
capability = &ucan.SimpleCapability{
Action: scope.UCANActions[0],
}
} else {
capability = &ucan.MultiCapability{
Actions: scope.UCANActions,
}
}
// Create resource based on scope type and context
resourceValue := m.resolveResourceValue(scope, userDID, resourceContext)
resource := &SimpleResource{
Scheme: scope.ResourceType,
Value: resourceValue,
}
return ucan.Attenuation{
Capability: capability,
Resource: resource,
}
}
func (m *ScopeMapper) resolveResourceValue(
scope *OAuth2ScopeDefinition,
userDID string,
context map[string]string,
) string {
switch scope.ResourceType {
case "did":
return fmt.Sprintf("did:sonr:%s", userDID)
case "vault":
if vaultAddr, exists := context["vault_address"]; exists {
return vaultAddr
}
return fmt.Sprintf("vault:%s", userDID)
case "service":
if serviceID, exists := context["service_id"]; exists {
return serviceID
}
return "service:*"
case "dwn":
if dwnID, exists := context["dwn_id"]; exists {
return dwnID
}
return fmt.Sprintf("dwn:%s", userDID)
default:
return "*"
}
}
// SimpleResource implements the Resource interface for OAuth scopes
type SimpleResource struct {
Scheme string
Value string
}
func (r *SimpleResource) GetScheme() string {
return r.Scheme
}
func (r *SimpleResource) GetValue() string {
return r.Value
}
func (r *SimpleResource) GetURI() string {
return fmt.Sprintf("%s://%s", r.Scheme, r.Value)
}
func (r *SimpleResource) Matches(other ucan.Resource) bool {
if r.Scheme != other.GetScheme() {
return false
}
// Wildcard matching
if r.Value == "*" || other.GetValue() == "*" {
return true
}
// Exact match
return r.Value == other.GetValue()
}
-449
View File
@@ -1,449 +0,0 @@
package handlers
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
// PKCE challenge methods
PKCEMethodS256 = "S256"
PKCEMethodPlain = "plain"
)
// PKCEValidator handles PKCE validation
type PKCEValidator struct{}
// NewPKCEValidator creates a new PKCE validator
func NewPKCEValidator() *PKCEValidator {
return &PKCEValidator{}
}
// GeneratePKCEPair generates a PKCE verifier and challenge pair
func (p *PKCEValidator) GeneratePKCEPair() (verifier, challenge string, err error) {
// Generate cryptographically secure verifier (43-128 characters)
verifierBytes := make([]byte, 32)
if _, err := rand.Read(verifierBytes); err != nil {
return "", "", fmt.Errorf("failed to generate PKCE verifier: %w", err)
}
verifier = base64.RawURLEncoding.EncodeToString(verifierBytes)
// Create S256 challenge
challenge = p.CreateChallenge(verifier, PKCEMethodS256)
return verifier, challenge, nil
}
// CreateChallenge creates a PKCE challenge from a verifier
func (p *PKCEValidator) CreateChallenge(verifier, method string) string {
switch method {
case PKCEMethodS256:
h := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(h[:])
case PKCEMethodPlain:
return verifier
default:
// Default to S256 for security
h := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(h[:])
}
}
// Validate validates a PKCE verifier against a challenge
func (p *PKCEValidator) Validate(verifier, challenge, method string) bool {
if method == "" {
method = PKCEMethodPlain
}
computed := p.CreateChallenge(verifier, method)
return subtle.ConstantTimeCompare([]byte(computed), []byte(challenge)) == 1
}
// computePKCEChallenge computes a PKCE challenge (helper function)
func computePKCEChallenge(verifier, method string) string {
validator := NewPKCEValidator()
return validator.CreateChallenge(verifier, method)
}
// CSRFProtector handles CSRF protection
type CSRFProtector struct {
secret []byte
}
// NewCSRFProtector creates a new CSRF protector
func NewCSRFProtector(secret string) *CSRFProtector {
return &CSRFProtector{
secret: []byte(secret),
}
}
// GenerateToken generates a CSRF token
func (c *CSRFProtector) GenerateToken(sessionID string) (string, error) {
// Create a unique token tied to the session
h := hmac.New(sha256.New, c.secret)
h.Write([]byte(sessionID))
h.Write([]byte(time.Now().Format(time.RFC3339)))
tokenBytes := h.Sum(nil)
return base64.RawURLEncoding.EncodeToString(tokenBytes), nil
}
// ValidateToken validates a CSRF token
func (c *CSRFProtector) ValidateToken(token, sessionID string) bool {
// Decode the token
tokenBytes, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return false
}
// Recreate the expected token
h := hmac.New(sha256.New, c.secret)
h.Write([]byte(sessionID))
// Note: In production, you'd want to include time validation
// and possibly store tokens with expiration
expectedBytes := h.Sum(nil)[:len(tokenBytes)]
return hmac.Equal(tokenBytes, expectedBytes)
}
// StateValidator validates OAuth state parameters
type StateValidator struct {
states map[string]*StateEntry
}
// StateEntry represents a stored state parameter
type StateEntry struct {
Value string
ClientID string
ExpiresAt time.Time
Used bool
}
// NewStateValidator creates a new state validator
func NewStateValidator() *StateValidator {
validator := &StateValidator{
states: make(map[string]*StateEntry),
}
// Start cleanup routine
go validator.cleanup()
return validator
}
// GenerateState generates a secure state parameter
func (s *StateValidator) GenerateState() (string, error) {
stateBytes := make([]byte, 32)
if _, err := rand.Read(stateBytes); err != nil {
return "", fmt.Errorf("failed to generate state: %w", err)
}
return base64.RawURLEncoding.EncodeToString(stateBytes), nil
}
// StoreState stores a state parameter for validation
func (s *StateValidator) StoreState(state, clientID string) {
s.states[state] = &StateEntry{
Value: state,
ClientID: clientID,
ExpiresAt: time.Now().Add(10 * time.Minute),
Used: false,
}
}
// ValidateState validates and consumes a state parameter
func (s *StateValidator) ValidateState(state, clientID string) bool {
entry, exists := s.states[state]
if !exists {
return false
}
// Check if expired
if time.Now().After(entry.ExpiresAt) {
delete(s.states, state)
return false
}
// Check if already used
if entry.Used {
return false
}
// Check client ID matches
if entry.ClientID != clientID {
return false
}
// Mark as used
entry.Used = true
return true
}
// cleanup removes expired states
func (s *StateValidator) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
for state, entry := range s.states {
if now.After(entry.ExpiresAt) {
delete(s.states, state)
}
}
}
}
// JWTClientAuthenticator handles JWT client authentication
type JWTClientAuthenticator struct {
clientRegistry *ClientRegistry
}
// NewJWTClientAuthenticator creates a new JWT client authenticator
func NewJWTClientAuthenticator(registry *ClientRegistry) *JWTClientAuthenticator {
return &JWTClientAuthenticator{
clientRegistry: registry,
}
}
// ValidateClientAssertion validates a JWT client assertion
func (j *JWTClientAuthenticator) ValidateClientAssertion(
assertion, expectedAudience string,
) (*OAuth2Client, error) {
// Parse the JWT without verification first to get the claims
token, _, err := jwt.NewParser().ParseUnverified(assertion, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse client assertion: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims format")
}
// Extract client ID from issuer and subject
clientID, ok := claims["iss"].(string)
if !ok {
return nil, fmt.Errorf("missing issuer claim")
}
subject, ok := claims["sub"].(string)
if !ok || subject != clientID {
return nil, fmt.Errorf("issuer and subject must match")
}
// Validate audience
audience, ok := claims["aud"].(string)
if !ok || audience != expectedAudience {
return nil, fmt.Errorf("invalid audience")
}
// Validate expiration
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("assertion expired")
}
} else {
return nil, fmt.Errorf("missing expiration")
}
// Validate not before
if nbf, ok := claims["nbf"].(float64); ok {
if time.Now().Unix() < int64(nbf) {
return nil, fmt.Errorf("assertion not yet valid")
}
}
// Validate issued at
if iat, ok := claims["iat"].(float64); ok {
// Check that the assertion is not too old (5 minutes max)
if time.Now().Unix()-int64(iat) > 300 {
return nil, fmt.Errorf("assertion too old")
}
}
// Validate JTI for replay protection
if jti, ok := claims["jti"].(string); !ok || jti == "" {
return nil, fmt.Errorf("missing jti claim")
}
// TODO: Store and check JTI to prevent replay attacks
// Get the client
client, err := j.clientRegistry.GetClient(clientID)
if err != nil {
return nil, fmt.Errorf("unknown client: %w", err)
}
// TODO: Verify the JWT signature using the client's registered public key
// This requires storing client public keys in the registry
return client, nil
}
// SecureTokenGenerator generates cryptographically secure tokens
type SecureTokenGenerator struct {
entropy int // bits of entropy
}
// NewSecureTokenGenerator creates a new secure token generator
func NewSecureTokenGenerator(entropyBits int) *SecureTokenGenerator {
if entropyBits < 128 {
entropyBits = 256 // Default to 256 bits for security
}
return &SecureTokenGenerator{
entropy: entropyBits,
}
}
// GenerateToken generates a secure random token
func (g *SecureTokenGenerator) GenerateToken() (string, error) {
bytes := make([]byte, g.entropy/8)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate secure token: %w", err)
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
// GenerateHexToken generates a secure random token in hex format
func (g *SecureTokenGenerator) GenerateHexToken() (string, error) {
bytes := make([]byte, g.entropy/8)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate secure token: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// RateLimiter implements rate limiting for OAuth endpoints
type RateLimiter struct {
attempts map[string][]time.Time
maxAttempts int
window time.Duration
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(maxAttempts int, window time.Duration) *RateLimiter {
limiter := &RateLimiter{
attempts: make(map[string][]time.Time),
maxAttempts: maxAttempts,
window: window,
}
// Start cleanup routine
go limiter.cleanup()
return limiter
}
// Allow checks if a request should be allowed
func (r *RateLimiter) Allow(key string) bool {
now := time.Now()
windowStart := now.Add(-r.window)
// Get attempts for this key
attempts := r.attempts[key]
// Filter out attempts outside the window
validAttempts := []time.Time{}
for _, attempt := range attempts {
if attempt.After(windowStart) {
validAttempts = append(validAttempts, attempt)
}
}
// Check if under limit
if len(validAttempts) >= r.maxAttempts {
return false
}
// Add this attempt
validAttempts = append(validAttempts, now)
r.attempts[key] = validAttempts
return true
}
// cleanup removes old entries
func (r *RateLimiter) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
windowStart := now.Add(-r.window)
for key, attempts := range r.attempts {
validAttempts := []time.Time{}
for _, attempt := range attempts {
if attempt.After(windowStart) {
validAttempts = append(validAttempts, attempt)
}
}
if len(validAttempts) == 0 {
delete(r.attempts, key)
} else {
r.attempts[key] = validAttempts
}
}
}
}
// OriginValidator validates request origins for CORS
type OriginValidator struct {
allowedOrigins map[string]bool
allowSubdomains bool
}
// NewOriginValidator creates a new origin validator
func NewOriginValidator(origins []string, allowSubdomains bool) *OriginValidator {
validator := &OriginValidator{
allowedOrigins: make(map[string]bool),
allowSubdomains: allowSubdomains,
}
for _, origin := range origins {
validator.allowedOrigins[origin] = true
}
return validator
}
// IsAllowed checks if an origin is allowed
func (o *OriginValidator) IsAllowed(origin string) bool {
// Direct match
if o.allowedOrigins[origin] {
return true
}
// Check subdomain matching if enabled
if o.allowSubdomains {
for allowed := range o.allowedOrigins {
if o.isSubdomainOf(origin, allowed) {
return true
}
}
}
return false
}
// isSubdomainOf checks if origin is a subdomain of allowed
func (o *OriginValidator) isSubdomainOf(origin, allowed string) bool {
// Simple subdomain check
// In production, use proper URL parsing
if strings.HasPrefix(allowed, "*.") {
domain := strings.TrimPrefix(allowed, "*")
return strings.HasSuffix(origin, domain)
}
return false
}
-650
View File
@@ -1,650 +0,0 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// TokenExchangeHandler implements RFC 8693 OAuth 2.0 Token Exchange
type TokenExchangeHandler struct {
delegator *UCANDelegator
signer *BlockchainUCANSigner
tokenStore TokenStore
clientStore ClientStore
}
// TokenStore interface for token persistence
type TokenStore interface {
GetToken(ctx context.Context, tokenID string) (*StoredToken, error)
StoreToken(ctx context.Context, token *StoredToken) error
RevokeToken(ctx context.Context, tokenID string) error
}
// ClientStore interface for OAuth client information
type ClientStore interface {
GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
ValidateClientCredentials(ctx context.Context, clientID, clientSecret string) error
}
// StoredToken represents a stored OAuth token
type StoredToken struct {
TokenID string `json:"token_id"`
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Scopes []string `json:"scopes"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did,omitempty"`
UCANToken string `json:"ucan_token"`
}
// TokenExchangeRequest represents an RFC 8693 token exchange request
type TokenExchangeRequest struct {
GrantType string `json:"grant_type"`
Resource string `json:"resource,omitempty"`
Audience string `json:"audience,omitempty"`
Scope string `json:"scope,omitempty"`
RequestedTokenType string `json:"requested_token_type,omitempty"`
SubjectToken string `json:"subject_token"`
SubjectTokenType string `json:"subject_token_type"`
ActorToken string `json:"actor_token,omitempty"`
ActorTokenType string `json:"actor_token_type,omitempty"`
}
// TokenExchangeResponse represents an RFC 8693 token exchange response
type TokenExchangeResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in,omitempty"`
Scope string `json:"scope,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// Token type identifiers from RFC 8693
const (
TokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token"
TokenTypeRefreshToken = "urn:ietf:params:oauth:token-type:refresh_token"
TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token"
TokenTypeSAML1 = "urn:ietf:params:oauth:token-type:saml1"
TokenTypeSAML2 = "urn:ietf:params:oauth:token-type:saml2"
TokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt"
TokenTypeUCAN = "urn:x-oath:params:oauth:token-type:ucan"
)
// NewTokenExchangeHandler creates a new token exchange handler
func NewTokenExchangeHandler(
delegator *UCANDelegator,
signer *BlockchainUCANSigner,
tokenStore TokenStore,
clientStore ClientStore,
) *TokenExchangeHandler {
return &TokenExchangeHandler{
delegator: delegator,
signer: signer,
tokenStore: tokenStore,
clientStore: clientStore,
}
}
// HandleTokenExchange handles RFC 8693 token exchange requests
func (h *TokenExchangeHandler) HandleTokenExchange(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse request
var req TokenExchangeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.sendError(w, "invalid_request", "Failed to parse request body")
return
}
// Validate grant type
if req.GrantType != "urn:ietf:params:oauth:grant-type:token-exchange" {
h.sendError(w, "unsupported_grant_type", "Only token-exchange grant type is supported")
return
}
// Validate required parameters
if req.SubjectToken == "" || req.SubjectTokenType == "" {
h.sendError(w, "invalid_request", "Missing required parameters")
return
}
// Authenticate client
clientID, clientSecret, ok := r.BasicAuth()
if !ok {
h.sendError(w, "invalid_client", "Client authentication required")
return
}
ctx := r.Context()
if err := h.clientStore.ValidateClientCredentials(ctx, clientID, clientSecret); err != nil {
h.sendError(w, "invalid_client", "Client authentication failed")
return
}
// Get client information
client, err := h.clientStore.GetClient(ctx, clientID)
if err != nil {
h.sendError(w, "invalid_client", "Client not found")
return
}
// Process token exchange based on token types
response, err := h.processTokenExchange(ctx, &req, client)
if err != nil {
h.sendError(w, "invalid_request", err.Error())
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
json.NewEncoder(w).Encode(response)
}
// processTokenExchange processes the token exchange based on token types
func (h *TokenExchangeHandler) processTokenExchange(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
) (*TokenExchangeResponse, error) {
// Determine requested token type (default to access token)
requestedType := req.RequestedTokenType
if requestedType == "" {
requestedType = TokenTypeAccessToken
}
// Handle different subject token types
switch req.SubjectTokenType {
case TokenTypeAccessToken:
return h.exchangeAccessToken(ctx, req, client, requestedType)
case TokenTypeRefreshToken:
return h.exchangeRefreshToken(ctx, req, client, requestedType)
case TokenTypeJWT:
return h.exchangeJWT(ctx, req, client, requestedType)
case TokenTypeUCAN:
return h.exchangeUCAN(ctx, req, client, requestedType)
default:
return nil, fmt.Errorf("unsupported subject token type: %s", req.SubjectTokenType)
}
}
// exchangeAccessToken exchanges an access token for a new token
func (h *TokenExchangeHandler) exchangeAccessToken(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Retrieve the subject token
storedToken, err := h.tokenStore.GetToken(ctx, req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid subject token")
}
// Validate token hasn't expired
if time.Now().After(storedToken.ExpiresAt) {
return nil, fmt.Errorf("subject token has expired")
}
// Parse requested scopes (default to original scopes)
requestedScopes := storedToken.Scopes
if req.Scope != "" {
requestedScopes = strings.Split(req.Scope, " ")
// Validate requested scopes are subset of original
if !h.isScopeSubset(requestedScopes, storedToken.Scopes) {
return nil, fmt.Errorf("requested scopes exceed original token scopes")
}
}
// Determine audience (default to requested audience or client ID)
audience := req.Audience
if audience == "" {
// Try to extract DID from client metadata
if clientDID, ok := client.Metadata["client_did"]; ok {
audience = clientDID
} else {
audience = client.ClientID
}
}
// Create new UCAN delegation based on requested type
switch requestedType {
case TokenTypeUCAN:
return h.createUCANResponse(
ctx,
storedToken.UserDID,
audience,
requestedScopes,
storedToken.UCANToken,
)
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, storedToken.UserDID, audience, requestedScopes)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// exchangeRefreshToken exchanges a refresh token for new tokens
func (h *TokenExchangeHandler) exchangeRefreshToken(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Retrieve the refresh token
storedToken, err := h.tokenStore.GetToken(ctx, req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
// Validate it's actually a refresh token
if storedToken.TokenType != "refresh_token" {
return nil, fmt.Errorf("token is not a refresh token")
}
// Create new tokens with same scopes
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
return h.createAccessTokenResponse(ctx, storedToken.UserDID, clientDID, storedToken.Scopes)
}
// exchangeJWT exchanges a JWT for a UCAN token
func (h *TokenExchangeHandler) exchangeJWT(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Verify the JWT
ucanToken, err := h.signer.VerifySignature(req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid JWT: %w", err)
}
// Extract scopes from JWT claims
scopes := h.extractScopesFromUCAN(ucanToken)
// Create response based on requested type
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
switch requestedType {
case TokenTypeUCAN:
return h.createUCANResponse(ctx, ucanToken.Issuer, clientDID, scopes, req.SubjectToken)
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, ucanToken.Issuer, clientDID, scopes)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// exchangeUCAN exchanges a UCAN token for another token type
func (h *TokenExchangeHandler) exchangeUCAN(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Verify the UCAN token
ucanToken, err := h.signer.VerifySignature(req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid UCAN token: %w", err)
}
// Validate delegation chain if actor token is provided
if req.ActorToken != "" {
actorToken, err := h.signer.VerifySignature(req.ActorToken)
if err != nil {
return nil, fmt.Errorf("invalid actor token: %w", err)
}
// Validate actor can act on behalf of subject
if actorToken.Audience != ucanToken.Issuer {
return nil, fmt.Errorf("actor token audience doesn't match subject issuer")
}
}
// Extract scopes from UCAN
scopes := h.extractScopesFromUCAN(ucanToken)
// Handle impersonation/delegation if actor token is present
issuer := ucanToken.Issuer
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
if req.ActorToken != "" {
// Actor is performing action on behalf of subject
issuer = clientDID // Actor becomes the new issuer
}
// Create response based on requested type
switch requestedType {
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, issuer, clientDID, scopes)
case TokenTypeUCAN:
// Create delegated UCAN with proof chain
proofs := []ucan.Proof{ucan.Proof(req.SubjectToken)}
if req.ActorToken != "" {
proofs = append(proofs, ucan.Proof(req.ActorToken))
}
return h.createDelegatedUCANResponse(ctx, issuer, clientDID, scopes, proofs)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// createUCANResponse creates a UCAN token response
func (h *TokenExchangeHandler) createUCANResponse(
ctx context.Context,
issuer, audience string,
scopes []string,
proof string,
) (*TokenExchangeResponse, error) {
// Create UCAN token with delegation
ucanToken, err := h.delegator.CreateDelegation(
issuer,
audience,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return nil, fmt.Errorf("failed to create UCAN token: %w", err)
}
// Add proof if provided
if proof != "" {
ucanToken.Proofs = []ucan.Proof{ucan.Proof(proof)}
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
return &TokenExchangeResponse{
AccessToken: signedToken,
IssuedTokenType: TokenTypeUCAN,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}, nil
}
// createDelegatedUCANResponse creates a delegated UCAN token with proof chain
func (h *TokenExchangeHandler) createDelegatedUCANResponse(
ctx context.Context,
issuer, audience string,
scopes []string,
proofs []ucan.Proof,
) (*TokenExchangeResponse, error) {
// Build resource context
resourceContext := map[string]string{
"delegation_type": "token_exchange",
"issued_at": fmt.Sprintf("%d", time.Now().Unix()),
}
// Map scopes to attenuations
attenuations := h.delegator.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
// Create UCAN token with proof chain
ucanToken := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(time.Hour).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: h.createTokenExchangeFact(scopes),
},
},
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return nil, fmt.Errorf("failed to sign delegated UCAN token: %w", err)
}
return &TokenExchangeResponse{
AccessToken: signedToken,
IssuedTokenType: TokenTypeUCAN,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}, nil
}
// createAccessTokenResponse creates a standard OAuth access token response
func (h *TokenExchangeHandler) createAccessTokenResponse(
ctx context.Context,
userDID, clientID string,
scopes []string,
) (*TokenExchangeResponse, error) {
// Create UCAN-backed access token
ucanToken, err := h.delegator.CreateDelegation(
userDID,
clientID,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return nil, fmt.Errorf("failed to create access token: %w", err)
}
// Generate token ID
tokenID := h.generateTokenID()
// Store token
storedToken := &StoredToken{
TokenID: tokenID,
TokenType: "access_token",
AccessToken: tokenID,
ExpiresAt: time.Now().Add(time.Hour),
Scopes: scopes,
ClientID: clientID,
UserDID: userDID,
UCANToken: ucanToken.Raw,
}
if err := h.tokenStore.StoreToken(ctx, storedToken); err != nil {
return nil, fmt.Errorf("failed to store token: %w", err)
}
// Generate refresh token
refreshTokenID := h.generateTokenID()
refreshToken := &StoredToken{
TokenID: refreshTokenID,
TokenType: "refresh_token",
RefreshToken: refreshTokenID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
Scopes: scopes,
ClientID: clientID,
UserDID: userDID,
}
if err := h.tokenStore.StoreToken(ctx, refreshToken); err != nil {
// Non-fatal, continue without refresh token
refreshTokenID = ""
}
response := &TokenExchangeResponse{
AccessToken: tokenID,
IssuedTokenType: TokenTypeAccessToken,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}
if refreshTokenID != "" {
response.RefreshToken = refreshTokenID
}
return response, nil
}
// extractScopesFromUCAN extracts OAuth scopes from UCAN attenuations
func (h *TokenExchangeHandler) extractScopesFromUCAN(token *ucan.Token) []string {
scopeMap := make(map[string]bool)
for _, att := range token.Attenuations {
scheme := att.Resource.GetScheme()
actions := att.Capability.GetActions()
// Map UCAN capabilities back to OAuth scopes
for _, action := range actions {
scope := h.mapUCANToScope(scheme, action)
if scope != "" {
scopeMap[scope] = true
}
}
}
scopes := make([]string, 0, len(scopeMap))
for scope := range scopeMap {
scopes = append(scopes, scope)
}
return scopes
}
// mapUCANToScope maps UCAN capability to OAuth scope
func (h *TokenExchangeHandler) mapUCANToScope(scheme, action string) string {
// Reverse mapping from UCAN to OAuth scopes
switch scheme {
case "vault":
switch action {
case "read":
return "vault:read"
case "write":
return "vault:write"
case "sign":
return "vault:sign"
case "*", "admin":
return "vault:admin"
}
case "service", "svc":
switch action {
case "read":
return "service:read"
case "write":
return "service:write"
case "*", "admin":
return "service:manage"
}
case "did":
switch action {
case "read":
return "did:read"
case "write", "update":
return "did:write"
}
case "dwn":
switch action {
case "read":
return "dwn:read"
case "write":
return "dwn:write"
}
}
// Default mapping
return fmt.Sprintf("%s:%s", scheme, action)
}
// isScopeSubset checks if requested scopes are subset of allowed scopes
func (h *TokenExchangeHandler) isScopeSubset(requested, allowed []string) bool {
allowedMap := make(map[string]bool)
for _, scope := range allowed {
allowedMap[scope] = true
}
for _, scope := range requested {
if !allowedMap[scope] {
// Check if parent scope is allowed
if !h.isParentScopeAllowed(scope, allowed) {
return false
}
}
}
return true
}
// isParentScopeAllowed checks if a parent scope grants the requested scope
func (h *TokenExchangeHandler) isParentScopeAllowed(requested string, allowed []string) bool {
for _, scope := range allowed {
if h.delegator.scopeMapper.IsHierarchicalScope(scope, requested) {
return true
}
}
return false
}
// createTokenExchangeFact creates a fact for token exchange
func (h *TokenExchangeHandler) createTokenExchangeFact(scopes []string) json.RawMessage {
fact := map[string]any{
"type": "token_exchange",
"scopes": scopes,
"issued_at": time.Now().Unix(),
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
// generateTokenID generates a unique token identifier
func (h *TokenExchangeHandler) generateTokenID() string {
// In production, use a proper UUID or random generator
return fmt.Sprintf("tok_%d_%s", time.Now().UnixNano(), h.randomString(16))
}
// randomString generates a random string of specified length
func (h *TokenExchangeHandler) randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(result)
}
// sendError sends an OAuth error response
func (h *TokenExchangeHandler) sendError(
w http.ResponseWriter,
errorCode, errorDescription string,
) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
w.WriteHeader(http.StatusBadRequest)
response := map[string]string{
"error": errorCode,
"error_description": errorDescription,
}
json.NewEncoder(w).Encode(response)
}
-269
View File
@@ -1,269 +0,0 @@
package handlers
import (
"encoding/json"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// OAuth2Client represents a registered OAuth2 client application
type OAuth2Client struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientType string `json:"client_type"` // "confidential" or "public"
RedirectURIs []string `json:"redirect_uris"`
AllowedScopes []string `json:"allowed_scopes"`
AllowedGrants []string `json:"allowed_grants"`
TokenLifetime time.Duration `json:"token_lifetime"`
RequirePKCE bool `json:"require_pkce"`
TrustedClient bool `json:"trusted_client"`
RequiresConsent bool `json:"requires_consent"`
Metadata map[string]string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuth2AuthorizationRequest represents an OAuth2 authorization request
type OAuth2AuthorizationRequest struct {
ResponseType string `json:"response_type" form:"response_type" query:"response_type"`
ClientID string `json:"client_id" form:"client_id" query:"client_id"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri" query:"redirect_uri"`
Scope string `json:"scope" form:"scope" query:"scope"`
State string `json:"state" form:"state" query:"state"`
CodeChallenge string `json:"code_challenge" form:"code_challenge" query:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method" form:"code_challenge_method" query:"code_challenge_method"`
Nonce string `json:"nonce" form:"nonce" query:"nonce"`
Prompt string `json:"prompt" form:"prompt" query:"prompt"`
MaxAge int `json:"max_age" form:"max_age" query:"max_age"`
LoginHint string `json:"login_hint" form:"login_hint" query:"login_hint"`
}
// OAuth2TokenRequest represents an OAuth2 token exchange request
type OAuth2TokenRequest struct {
GrantType string `json:"grant_type" form:"grant_type"`
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
RefreshToken string `json:"refresh_token" form:"refresh_token"`
Scope string `json:"scope" form:"scope"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Assertion string `json:"assertion" form:"assertion"`
ClientAssertion string `json:"client_assertion" form:"client_assertion"`
ClientAssertionType string `json:"client_assertion_type" form:"client_assertion_type"`
}
// OAuth2TokenResponse represents an OAuth2 token response
type OAuth2TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
IDToken string `json:"id_token,omitempty"`
UCANToken string `json:"ucan_token,omitempty"` // UCAN delegation token
}
// OAuth2ErrorResponse represents an OAuth2 error response
type OAuth2ErrorResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
ErrorURI string `json:"error_uri,omitempty"`
State string `json:"state,omitempty"`
}
// OAuth2AuthorizationCode represents an authorization code with UCAN context
type OAuth2AuthorizationCode struct {
Code string `json:"code"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did"`
RedirectURI string `json:"redirect_uri"`
Scopes []string `json:"scopes"`
State string `json:"state"`
Nonce string `json:"nonce"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used"`
UCANContext *UCANAuthContext `json:"ucan_context"`
}
// UCANAuthContext holds UCAN-related data for authorization
type UCANAuthContext struct {
VaultAddress string `json:"vault_address"`
EnclaveDataCID string `json:"enclave_data_cid"`
DIDDocument json.RawMessage `json:"did_document"`
Capabilities []string `json:"capabilities"`
}
// OAuth2AccessToken represents an access token with embedded UCAN
type OAuth2AccessToken struct {
Token string `json:"token"`
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
ExpiresAt time.Time `json:"expires_at"`
IssuedAt time.Time `json:"issued_at"`
UCANToken *ucan.Token `json:"ucan_token"`
SessionID string `json:"session_id"`
TokenType string `json:"token_type"`
}
// OAuth2RefreshToken represents a refresh token
type OAuth2RefreshToken struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did"`
Scopes []string `json:"scopes"`
ExpiresAt time.Time `json:"expires_at"`
IssuedAt time.Time `json:"issued_at"`
RotationCount int `json:"rotation_count"`
}
// OAuth2Session extends OIDCSession with OAuth2-specific fields
type OAuth2Session struct {
SessionID string `json:"session_id"`
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
UCANToken *ucan.Token `json:"ucan_token"`
ConsentGiven bool `json:"consent_given"`
ConsentScopes []string `json:"consent_scopes"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuth2ConsentRequest represents a consent request for a client
type OAuth2ConsentRequest struct {
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
RequestedScopes []string `json:"requested_scopes"`
ConsentID string `json:"consent_id"`
CreatedAt time.Time `json:"created_at"`
}
// OAuth2ConsentResponse represents a user's consent response
type OAuth2ConsentResponse struct {
ConsentID string `json:"consent_id"`
Approved bool `json:"approved"`
ApprovedScopes []string `json:"approved_scopes"`
RememberChoice bool `json:"remember_choice"`
}
// OAuth2ScopeDefinition defines an OAuth scope and its UCAN mapping
type OAuth2ScopeDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
UCANActions []string `json:"ucan_actions"`
ResourceType string `json:"resource_type"`
RequiresAuth bool `json:"requires_auth"`
Sensitive bool `json:"sensitive"`
ParentScope string `json:"parent_scope,omitempty"`
ChildScopes []string `json:"child_scopes,omitempty"`
Metadata map[string]any `json:"metadata"`
}
// OAuth2ClientRegistrationRequest represents a dynamic client registration request
type OAuth2ClientRegistrationRequest struct {
ClientName string `json:"client_name"`
ClientType string `json:"client_type"` // "confidential" or "public"
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scopes string `json:"scopes,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TOSUri string `json:"tos_uri,omitempty"`
Contacts []string `json:"contacts,omitempty"`
}
// OAuth2ClientRegistrationResponse represents a successful client registration
type OAuth2ClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
}
// OAuth2IntrospectionRequest represents a token introspection request
type OAuth2IntrospectionRequest struct {
Token string `json:"token" form:"token"`
TokenTypeHint string `json:"token_type_hint" form:"token_type_hint"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
}
// OAuth2IntrospectionResponse represents a token introspection response
type OAuth2IntrospectionResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
Username string `json:"username,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Audience []string `json:"aud,omitempty"`
Issuer string `json:"iss,omitempty"`
JWTID string `json:"jti,omitempty"`
UCANToken string `json:"ucan_token,omitempty"`
}
// OAuth2RevocationRequest represents a token revocation request
type OAuth2RevocationRequest struct {
Token string `json:"token" form:"token"`
TokenTypeHint string `json:"token_type_hint" form:"token_type_hint"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
}
// OAuth2Config extends OIDCConfig with OAuth2-specific endpoints
type OAuth2Config struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
JWKSEndpoint string `json:"jwks_uri"`
RegistrationEndpoint string `json:"registration_endpoint"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
ServiceDocumentation string `json:"service_documentation,omitempty"`
UILocalesSupported []string `json:"ui_locales_supported,omitempty"`
OpPolicyURI string `json:"op_policy_uri,omitempty"`
OpTosURI string `json:"op_tos_uri,omitempty"`
UCANSupported bool `json:"ucan_supported"` // Custom field for UCAN support
}
// TokenValidationResult represents the result of token validation
type TokenValidationResult struct {
Valid bool `json:"valid"`
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
UCANToken *ucan.Token `json:"ucan_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Error string `json:"error,omitempty"`
}
-466
View File
@@ -1,466 +0,0 @@
package handlers
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// OIDCProvider manages OIDC operations
type OIDCProvider struct {
mu sync.RWMutex
codes map[string]*AuthorizationCode
sessions map[string]*OIDCSession
config any // TODO: Use bridge.OIDCProviderConfig
}
var (
oidcProvider = &OIDCProvider{
codes: make(map[string]*AuthorizationCode),
sessions: make(map[string]*OIDCSession),
}
codeExpiration = 10 * time.Minute
)
// SetOIDCConfig sets the OIDC provider configuration
func SetOIDCConfig(config any) {
oidcProvider.mu.Lock()
defer oidcProvider.mu.Unlock()
oidcProvider.config = config
}
// GetOIDCDiscovery returns OIDC discovery configuration
func GetOIDCDiscovery(c echo.Context) error {
config := &OIDCConfig{
Issuer: "https://localhost:8080",
AuthorizationEndpoint: "https://localhost:8080/oidc/authorize",
TokenEndpoint: "https://localhost:8080/oidc/token",
UserInfoEndpoint: "https://localhost:8080/oidc/userinfo",
JWKSEndpoint: "https://localhost:8080/oidc/jwks",
RevocationEndpoint: "https://localhost:8080/oidc/revoke",
IntrospectionEndpoint: "https://localhost:8080/oidc/introspect",
ScopesSupported: []string{
"openid", "profile", "email", "did", "vault", "offline_access",
},
ResponseTypesSupported: []string{
"code", "id_token", "code id_token",
},
GrantTypesSupported: []string{
"authorization_code", "refresh_token", "client_credentials",
},
SubjectTypesSupported: []string{
"public", "pairwise",
},
IDTokenSigningAlgValuesSupported: []string{
"ES256", "RS256",
},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_post", "client_secret_basic", "none",
},
ClaimsSupported: []string{
"sub", "name", "preferred_username", "email", "email_verified",
"did", "vault_id", "updated_at",
},
CodeChallengeMethodsSupported: []string{
"S256", "plain",
},
}
return c.JSON(http.StatusOK, config)
}
// HandleOIDCAuthorization handles OIDC authorization requests
func HandleOIDCAuthorization(c echo.Context) error {
// Parse request parameters from query string for GET or form for POST
req := OIDCAuthorizationRequest{
ResponseType: c.QueryParam("response_type"),
ClientID: c.QueryParam("client_id"),
RedirectURI: c.QueryParam("redirect_uri"),
Scope: c.QueryParam("scope"),
State: c.QueryParam("state"),
Nonce: c.QueryParam("nonce"),
CodeChallenge: c.QueryParam("code_challenge"),
CodeChallengeMethod: c.QueryParam("code_challenge_method"),
}
// If no query params, try to bind from body (for POST requests)
if req.ResponseType == "" {
_ = c.Bind(&req) // Ignore bind errors and continue
}
// Validate required parameters
if req.ResponseType == "" || req.ClientID == "" || req.RedirectURI == "" || req.Scope == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing required parameters",
})
}
// Validate response type
if req.ResponseType != "code" && req.ResponseType != "id_token" &&
req.ResponseType != "code id_token" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "unsupported_response_type",
"error_description": "Response type not supported",
})
}
// Validate scope includes openid
if !strings.Contains(req.Scope, "openid") {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_scope",
"error_description": "Scope must include 'openid'",
})
}
// TODO: Validate client_id and redirect_uri against registered clients
// For now, assume user is authenticated (in production, redirect to login)
userDID := c.Get("user_did")
if userDID == nil {
// Redirect to WebAuthn authentication
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "authentication_required",
"error_description": "User authentication required",
})
}
// Generate authorization code
code := generateAuthorizationCode()
// Store authorization code
authCode := &AuthorizationCode{
Code: code,
ClientID: req.ClientID,
RedirectURI: req.RedirectURI,
UserDID: userDID.(string),
Scope: req.Scope,
Nonce: req.Nonce,
CodeChallenge: req.CodeChallenge,
CodeChallengeMethod: req.CodeChallengeMethod,
ExpiresAt: time.Now().Add(codeExpiration),
Used: false,
}
oidcProvider.mu.Lock()
oidcProvider.codes[code] = authCode
oidcProvider.mu.Unlock()
// Build redirect URL
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", req.RedirectURI, code, req.State)
return c.Redirect(http.StatusFound, redirectURL)
}
// HandleOIDCToken handles OIDC token requests
func HandleOIDCToken(c echo.Context) error {
var req OIDCTokenRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Invalid token request",
})
}
// Handle different grant types
switch req.GrantType {
case "authorization_code":
return handleAuthorizationCodeGrant(c, &req)
case "refresh_token":
return handleRefreshTokenGrant(c, &req)
case "client_credentials":
return handleClientCredentialsGrant(c, &req)
default:
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "unsupported_grant_type",
"error_description": "Grant type not supported",
})
}
}
// handleAuthorizationCodeGrant processes authorization code grant
func handleAuthorizationCodeGrant(c echo.Context, req *OIDCTokenRequest) error {
if req.Code == "" || req.RedirectURI == "" || req.ClientID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing required parameters",
})
}
// Retrieve and validate authorization code
oidcProvider.mu.Lock()
authCode, exists := oidcProvider.codes[req.Code]
if exists {
delete(oidcProvider.codes, req.Code) // Single use
}
oidcProvider.mu.Unlock()
if !exists {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Invalid authorization code",
})
}
// Validate code hasn't expired
if time.Now().After(authCode.ExpiresAt) {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Authorization code expired",
})
}
// Validate code hasn't been used
if authCode.Used {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Authorization code already used",
})
}
// Validate client and redirect URI
if authCode.ClientID != req.ClientID || authCode.RedirectURI != req.RedirectURI {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Invalid client or redirect URI",
})
}
// Validate PKCE if present
if authCode.CodeChallenge != "" {
if req.CodeVerifier == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Code verifier required",
})
}
if !verifyPKCE(req.CodeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Invalid code verifier",
})
}
}
// Mark code as used
authCode.Used = true
// Generate tokens
accessToken := generateAccessToken(authCode.UserDID)
refreshToken := generateRefreshToken()
idToken, err := generateIDToken(authCode.UserDID, authCode.ClientID, authCode.Nonce)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "server_error",
"error_description": "Failed to generate ID token",
})
}
// Create session
session := &OIDCSession{
SessionID: generateSessionID(),
UserDID: authCode.UserDID,
ClientID: authCode.ClientID,
Scope: authCode.Scope,
Nonce: authCode.Nonce,
AccessToken: accessToken,
RefreshToken: refreshToken,
IDToken: idToken,
ExpiresAt: time.Now().Add(time.Hour),
CreatedAt: time.Now(),
}
oidcProvider.mu.Lock()
oidcProvider.sessions[accessToken] = session
oidcProvider.mu.Unlock()
// Return tokens
response := &OIDCTokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: refreshToken,
IDToken: idToken,
Scope: authCode.Scope,
}
return c.JSON(http.StatusOK, response)
}
// handleRefreshTokenGrant processes refresh token grant
func handleRefreshTokenGrant(c echo.Context, req *OIDCTokenRequest) error {
// TODO: Implement refresh token grant
return c.JSON(http.StatusNotImplemented, map[string]string{
"error": "unsupported_grant_type",
"error_description": "Refresh token grant not yet implemented",
})
}
// handleClientCredentialsGrant processes client credentials grant
func handleClientCredentialsGrant(c echo.Context, req *OIDCTokenRequest) error {
// TODO: Implement client credentials grant
return c.JSON(http.StatusNotImplemented, map[string]string{
"error": "unsupported_grant_type",
"error_description": "Client credentials grant not yet implemented",
})
}
// HandleOIDCUserInfo handles OIDC userinfo requests
func HandleOIDCUserInfo(c echo.Context) error {
// Extract access token from Authorization header
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"error_description": "Invalid access token",
})
}
accessToken := strings.TrimPrefix(authHeader, "Bearer ")
// Validate access token
oidcProvider.mu.RLock()
session, exists := oidcProvider.sessions[accessToken]
oidcProvider.mu.RUnlock()
if !exists {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"error_description": "Invalid or expired access token",
})
}
// Check if token is expired
if time.Now().After(session.ExpiresAt) {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"error_description": "Access token expired",
})
}
// TODO: Fetch actual user info from DID document or database
userInfo := &OIDCUserInfo{
Subject: session.UserDID,
PreferredUsername: "user", // TODO: Get from DID document
DID: session.UserDID,
UpdatedAt: time.Now().Unix(),
}
// Add additional claims based on scope
if strings.Contains(session.Scope, "profile") {
userInfo.Name = "User Name" // TODO: Get from DID document
}
if strings.Contains(session.Scope, "email") {
userInfo.Email = "user@example.com" // TODO: Get from DID document
userInfo.EmailVerified = true
}
if strings.Contains(session.Scope, "vault") {
userInfo.VaultID = "vault_" + session.UserDID // TODO: Get actual vault ID
}
return c.JSON(http.StatusOK, userInfo)
}
// HandleOIDCJWKS handles JWKS endpoint
func HandleOIDCJWKS(c echo.Context) error {
// TODO: Return actual public keys used for signing
jwks := &JWKSet{
Keys: []JWK{
{
KeyType: "EC",
Use: "sig",
KeyID: "1",
Algorithm: "ES256",
Curve: "P-256",
// TODO: Add actual public key coordinates
},
},
}
return c.JSON(http.StatusOK, jwks)
}
// Helper functions
func generateAuthorizationCode() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateAccessToken(userDID string) string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateRefreshToken() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateSessionID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateIDToken(userDID, clientID, nonce string) (string, error) {
claims := &DIDAuthClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: userDID,
Issuer: "https://localhost:8080",
Audience: []string{clientID},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
DID: userDID,
}
if nonce != "" {
claims.Extra = map[string]any{
"nonce": nonce,
}
}
// TODO: Sign with actual signing key
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("temporary-secret-key"))
}
func verifyPKCE(verifier, challenge, method string) bool {
var computed string
switch method {
case "S256":
h := sha256.Sum256([]byte(verifier))
computed = base64.RawURLEncoding.EncodeToString(h[:])
case "plain":
computed = verifier
default:
return false
}
return computed == challenge
}
-360
View File
@@ -1,360 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestOIDCDiscovery tests the OIDC discovery endpoint
func TestOIDCDiscovery(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/.well-known/openid-configuration", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := GetOIDCDiscovery(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var config OIDCConfig
err = json.Unmarshal(rec.Body.Bytes(), &config)
assert.NoError(t, err)
// Verify required fields
assert.NotEmpty(t, config.Issuer)
assert.NotEmpty(t, config.AuthorizationEndpoint)
assert.NotEmpty(t, config.TokenEndpoint)
assert.NotEmpty(t, config.UserInfoEndpoint)
assert.NotEmpty(t, config.JWKSEndpoint)
assert.Contains(t, config.ScopesSupported, "openid")
assert.Contains(t, config.ResponseTypesSupported, "code")
assert.Contains(t, config.GrantTypesSupported, "authorization_code")
}
// TestOIDCAuthorizationFlow tests the authorization code flow
func TestOIDCAuthorizationFlow(t *testing.T) {
e := echo.New()
t.Run("ValidAuthorizationRequest", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "code")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid profile")
q.Set("state", "test-state")
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Set authenticated user context
c.Set("user_did", "did:sonr:testuser")
c.Set("authenticated", true)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
// Should redirect with authorization code
assert.Equal(t, http.StatusFound, rec.Code)
location := rec.Header().Get("Location")
assert.Contains(t, location, "code=")
assert.Contains(t, location, "state=test-state")
})
t.Run("MissingRequiredParameters", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_request", errorResp["error"])
})
t.Run("InvalidResponseType", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "invalid")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
})
}
// TestOIDCTokenExchange tests the token endpoint
func TestOIDCTokenExchange(t *testing.T) {
e := echo.New()
// Setup: Create an authorization code
code := "test-auth-code"
authCode := &AuthorizationCode{
Code: code,
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
UserDID: "did:sonr:testuser",
Scope: "openid profile",
ExpiresAt: time.Now().Add(10 * time.Minute),
CodeChallenge: "test-challenge",
CodeChallengeMethod: "S256",
}
oidcProvider.mu.Lock()
oidcProvider.codes[code] = authCode
oidcProvider.mu.Unlock()
t.Run("ValidTokenExchange", func(t *testing.T) {
body := strings.NewReader("grant_type=authorization_code&code=" + code +
"&redirect_uri=http://localhost:3000/callback&client_id=test-client" +
"&code_verifier=test-verifier")
req := httptest.NewRequest(http.MethodPost, "/oidc/token", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: code,
RedirectURI: "http://localhost:3000/callback",
ClientID: "test-client",
CodeVerifier: "test-verifier",
}
err := handleAuthorizationCodeGrant(c, tokenReq)
assert.NoError(t, err)
if rec.Code == http.StatusOK {
var tokenResp OIDCTokenResponse
err = json.Unmarshal(rec.Body.Bytes(), &tokenResp)
assert.NoError(t, err)
assert.NotEmpty(t, tokenResp.AccessToken)
assert.NotEmpty(t, tokenResp.IDToken)
assert.Equal(t, "Bearer", tokenResp.TokenType)
}
})
t.Run("ExpiredAuthorizationCode", func(t *testing.T) {
expiredCode := "expired-code"
expiredAuthCode := &AuthorizationCode{
Code: expiredCode,
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
UserDID: "did:sonr:testuser",
ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired
}
oidcProvider.mu.Lock()
oidcProvider.codes[expiredCode] = expiredAuthCode
oidcProvider.mu.Unlock()
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: expiredCode,
RedirectURI: "http://localhost:3000/callback",
ClientID: "test-client",
}
req := httptest.NewRequest(http.MethodPost, "/oidc/token", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handleAuthorizationCodeGrant(c, tokenReq)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
})
}
// TestOIDCUserInfo tests the userinfo endpoint
func TestOIDCUserInfo(t *testing.T) {
e := echo.New()
// Setup: Create a session
accessToken := "test-access-token"
session := &OIDCSession{
SessionID: "test-session",
UserDID: "did:sonr:testuser",
ClientID: "test-client",
Scope: "openid profile email",
AccessToken: accessToken,
RefreshToken: "test-refresh-token",
ExpiresAt: time.Now().Add(1 * time.Hour),
CreatedAt: time.Now(),
}
oidcProvider.mu.Lock()
oidcProvider.sessions[accessToken] = session
oidcProvider.mu.Unlock()
t.Run("ValidUserInfoRequest", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/userinfo", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCUserInfo(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var userInfo map[string]any
err = json.Unmarshal(rec.Body.Bytes(), &userInfo)
assert.NoError(t, err)
assert.Equal(t, "did:sonr:testuser", userInfo["sub"])
})
t.Run("InvalidAccessToken", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/userinfo", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCUserInfo(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("MissingAuthorizationHeader", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/userinfo", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCUserInfo(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
}
// TestPKCEFlow tests PKCE (Proof Key for Code Exchange) implementation
func TestPKCEFlow(t *testing.T) {
e := echo.New()
// Generate PKCE parameters
codeVerifier := "test-code-verifier-string-that-is-long-enough"
codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" // SHA256 of verifier
t.Run("AuthorizationWithPKCE", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "code")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("code_challenge", codeChallenge)
q.Set("code_challenge_method", "S256")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user_did", "did:sonr:testuser")
c.Set("authenticated", true)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, rec.Code)
})
t.Run("TokenExchangeWithPKCE", func(t *testing.T) {
// Create auth code with PKCE
code := "pkce-auth-code"
authCode := &AuthorizationCode{
Code: code,
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
UserDID: "did:sonr:testuser",
CodeChallenge: codeChallenge,
CodeChallengeMethod: "S256",
ExpiresAt: time.Now().Add(10 * time.Minute),
}
oidcProvider.mu.Lock()
oidcProvider.codes[code] = authCode
oidcProvider.mu.Unlock()
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: code,
RedirectURI: "http://localhost:3000/callback",
ClientID: "test-client",
CodeVerifier: codeVerifier,
}
req := httptest.NewRequest(http.MethodPost, "/oidc/token", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handleAuthorizationCodeGrant(c, tokenReq)
require.NoError(t, err)
// With correct verifier, should succeed
if rec.Code != http.StatusOK {
t.Logf("Response: %s", rec.Body.String())
}
})
}
// TestRefreshTokenFlow tests refresh token functionality
func TestRefreshTokenFlow(t *testing.T) {
e := echo.New()
// Create initial session with refresh token
refreshToken := "test-refresh-token"
session := &OIDCSession{
SessionID: "test-session",
UserDID: "did:sonr:testuser",
ClientID: "test-client",
Scope: "openid profile offline_access",
AccessToken: "old-access-token",
RefreshToken: refreshToken,
ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired access token
CreatedAt: time.Now().Add(-2 * time.Hour),
}
oidcProvider.mu.Lock()
oidcProvider.sessions[refreshToken] = session
oidcProvider.mu.Unlock()
t.Run("ValidRefreshToken", func(t *testing.T) {
tokenReq := &OIDCTokenRequest{
GrantType: "refresh_token",
RefreshToken: refreshToken,
ClientID: "test-client",
}
req := httptest.NewRequest(http.MethodPost, "/oidc/token", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handleRefreshTokenGrant(c, tokenReq)
assert.NoError(t, err)
if rec.Code == http.StatusOK {
var tokenResp OIDCTokenResponse
err = json.Unmarshal(rec.Body.Bytes(), &tokenResp)
assert.NoError(t, err)
assert.NotEmpty(t, tokenResp.AccessToken)
assert.NotEqual(t, "old-access-token", tokenResp.AccessToken)
}
})
}
-384
View File
@@ -1,384 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// SIOPProvider manages Self-Issued OpenID Provider operations
type SIOPProvider struct {
// No persistent state needed for SIOP as it's self-issued
}
var siopProvider = &SIOPProvider{}
// HandleSIOPAuthorization handles SIOP authorization requests
func HandleSIOPAuthorization(c echo.Context) error {
var req SIOPRequest
// Check if request was already parsed by HandleSIOPRequest
if parsedReq := c.Get("siop_request"); parsedReq != nil {
req = *(parsedReq.(*SIOPRequest))
} else {
// Parse request parameters from query string for GET or form for POST
req = SIOPRequest{
ResponseType: c.QueryParam("response_type"),
ClientID: c.QueryParam("client_id"),
RedirectURI: c.QueryParam("redirect_uri"),
Scope: c.QueryParam("scope"),
Nonce: c.QueryParam("nonce"),
State: c.QueryParam("state"),
}
// Parse claims if provided
if claimsStr := c.QueryParam("claims"); claimsStr != "" {
_ = json.Unmarshal([]byte(claimsStr), &req.Claims) // Ignore claims parsing errors
}
// If no query params, try to bind from body (for POST requests)
if req.ResponseType == "" {
_ = c.Bind(&req) // Ignore bind errors and continue with empty request
}
}
// Validate required parameters
if req.ResponseType == "" || req.ClientID == "" || req.RedirectURI == "" ||
req.Scope == "" || req.Nonce == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing required parameters",
})
}
// Validate response type (SIOP v2 supports id_token)
if req.ResponseType != "id_token" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "unsupported_response_type",
"error_description": "SIOP only supports id_token response type",
})
}
// Validate scope includes openid
if !strings.Contains(req.Scope, "openid") {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_scope",
"error_description": "Scope must include 'openid'",
})
}
// Get user DID from context (assumes user is authenticated via WebAuthn)
userDID := c.Get("user_did")
if userDID == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "authentication_required",
"error_description": "User authentication required for SIOP",
})
}
// Generate Self-Issued ID Token
idToken, err := generateSelfIssuedIDToken(
userDID.(string),
req.ClientID,
req.Nonce,
req.RedirectURI,
req.Claims,
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "server_error",
"error_description": "Failed to generate self-issued ID token",
})
}
// Build response
response := &SIOPResponse{
IDToken: idToken,
State: req.State,
}
// If VP token is requested, include it
if req.Claims.VPToken != nil {
vpToken, err := generateVPToken(userDID.(string), req.Claims.VPToken)
if err == nil {
response.VPToken = vpToken
}
}
// Redirect back to client with response
redirectURL, err := buildSIOPRedirectURL(req.RedirectURI, response)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "server_error",
"error_description": "Failed to build redirect URL",
})
}
return c.Redirect(http.StatusFound, redirectURL)
}
// HandleSIOPRegistration handles dynamic client registration for SIOP
func HandleSIOPRegistration(c echo.Context) error {
var registration SIOPRegistration
if err := c.Bind(&registration); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Invalid registration request",
})
}
// Validate registration parameters
if registration.ClientName == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Client name is required",
})
}
// Generate client ID for the registration
clientID := generateClientID()
// Store registration (in production, persist this)
// For now, return the registration confirmation
response := map[string]any{
"client_id": clientID,
"client_name": registration.ClientName,
"client_purpose": registration.ClientPurpose,
"logo_uri": registration.LogoURI,
"subject_syntax_types_supported": []string{"did"},
"vp_formats": map[string]any{
"jwt_vp": map[string]any{
"alg": []string{"ES256", "EdDSA"},
},
"ldp_vp": map[string]any{
"proof_type": []string{"Ed25519Signature2018"},
},
},
}
return c.JSON(http.StatusOK, response)
}
// HandleSIOPMetadata returns SIOP provider metadata
func HandleSIOPMetadata(c echo.Context) error {
metadata := map[string]any{
"issuer": "https://self-issued.me/v2",
"authorization_endpoint": "openid://",
"response_types_supported": []string{"id_token"},
"scopes_supported": []string{
"openid",
"profile",
"email",
"did",
},
"subject_types_supported": []string{"pairwise"},
"id_token_signing_alg_values_supported": []string{"ES256", "EdDSA"},
"request_object_signing_alg_values_supported": []string{"ES256", "EdDSA"},
"subject_syntax_types_supported": []string{
"did:key",
"did:web",
"did:ion",
},
"vp_formats_supported": map[string]any{
"jwt_vp": map[string]any{
"alg_values_supported": []string{"ES256", "EdDSA"},
},
"ldp_vp": map[string]any{
"proof_type_values_supported": []string{"Ed25519Signature2018"},
},
},
}
return c.JSON(http.StatusOK, metadata)
}
// generateSelfIssuedIDToken generates a self-issued ID token
func generateSelfIssuedIDToken(
userDID, clientID, nonce, redirectURI string,
requestedClaims SIOPClaims,
) (string, error) {
// Create base claims
claims := jwt.MapClaims{
"iss": "https://self-issued.me/v2",
"sub": userDID,
"aud": clientID,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"nonce": nonce,
"_sd_alg": "sha-256", // Selective disclosure algorithm
"sub_jwk": map[string]any{
// TODO: Include actual public key from DID document
"kty": "EC",
"crv": "P-256",
"x": "placeholder_x",
"y": "placeholder_y",
},
}
// Add requested claims from ID token
if requestedClaims.IDToken != nil {
for claimName := range requestedClaims.IDToken {
// Add claim based on request
switch claimName {
case "email":
claims["email"] = "user@example.com" // TODO: Get from DID document
claims["email_verified"] = true
case "name":
claims["name"] = "User Name" // TODO: Get from DID document
case "did":
claims["did"] = userDID
}
}
}
// Sign with user's DID key (simplified for now)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("temporary-siop-key"))
}
// generateVPToken generates a Verifiable Presentation token
func generateVPToken(userDID string, vpClaims map[string]ClaimRequest) (string, error) {
// Create VP structure
vp := map[string]any{
"@context": []string{
"https://www.w3.org/2018/credentials/v1",
},
"type": []string{"VerifiablePresentation"},
"holder": userDID,
"verifiableCredential": []any{
// TODO: Include actual verifiable credentials
},
}
// Convert to JWT
vpJSON, err := json.Marshal(vp)
if err != nil {
return "", err
}
// Sign VP (simplified for now)
claims := jwt.MapClaims{
"vp": string(vpJSON),
"iss": userDID,
"aud": "verifier",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"nonce": generateNonce(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("temporary-vp-key"))
}
// buildSIOPRedirectURL builds the redirect URL with SIOP response
func buildSIOPRedirectURL(redirectURI string, response *SIOPResponse) (string, error) {
u, err := url.Parse(redirectURI)
if err != nil {
return "", err
}
// Add response parameters
q := u.Query()
q.Set("id_token", response.IDToken)
if response.VPToken != "" {
q.Set("vp_token", response.VPToken)
}
if response.State != "" {
q.Set("state", response.State)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// generateClientID generates a unique client ID
func generateClientID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return fmt.Sprintf("siop_client_%s", base64.RawURLEncoding.EncodeToString(bytes))
}
// generateNonce generates a nonce for tokens
func generateNonce() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
// ValidateSIOPRequest validates an incoming SIOP request
func ValidateSIOPRequest(requestURI string) (*SIOPRequest, error) {
// Parse the request URI
u, err := url.Parse(requestURI)
if err != nil {
return nil, fmt.Errorf("invalid request URI: %w", err)
}
// Validate that it's a proper URI with scheme and host
if u.Scheme == "" && u.Host == "" {
return nil, fmt.Errorf("invalid request URI: missing scheme or host")
}
// Extract parameters
q := u.Query()
req := &SIOPRequest{
ResponseType: q.Get("response_type"),
ClientID: q.Get("client_id"),
RedirectURI: q.Get("redirect_uri"),
Scope: q.Get("scope"),
Nonce: q.Get("nonce"),
State: q.Get("state"),
}
// Parse claims if present
if claimsStr := q.Get("claims"); claimsStr != "" {
if err := json.Unmarshal([]byte(claimsStr), &req.Claims); err != nil {
return nil, fmt.Errorf("invalid claims parameter: %w", err)
}
}
// Parse registration if present
if regStr := q.Get("registration"); regStr != "" {
if err := json.Unmarshal([]byte(regStr), &req.Registration); err != nil {
return nil, fmt.Errorf("invalid registration parameter: %w", err)
}
}
return req, nil
}
// HandleSIOPRequest processes a complete SIOP request flow
func HandleSIOPRequest(c echo.Context) error {
// Get request URI from query parameter
requestURI := c.QueryParam("request_uri")
if requestURI == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing request_uri parameter",
})
}
// Validate and parse the request
req, err := ValidateSIOPRequest(requestURI)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": err.Error(),
})
}
// Process as regular SIOP authorization
c.Set("siop_request", req)
return HandleSIOPAuthorization(c)
}
-353
View File
@@ -1,353 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
// TestSIOPAuthorization tests Self-Issued OpenID Provider authorization
func TestSIOPAuthorization(t *testing.T) {
e := echo.New()
t.Run("ValidSIOPRequest", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid did")
q.Set("nonce", "test-nonce")
q.Set("state", "test-state")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Set authenticated user DID
c.Set("user_did", "did:sonr:testuser")
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
// Should redirect with ID token
assert.Equal(t, http.StatusFound, rec.Code)
location := rec.Header().Get("Location")
assert.Contains(t, location, "id_token=")
assert.Contains(t, location, "state=test-state")
})
t.Run("InvalidResponseType", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "code") // SIOP only supports id_token
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "unsupported_response_type", errorResp["error"])
})
t.Run("MissingScopeOpenID", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "profile email") // Missing 'openid'
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_scope", errorResp["error"])
})
t.Run("UnauthenticatedUser", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// No user_did set - user not authenticated
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "authentication_required", errorResp["error"])
})
}
// TestSIOPRegistration tests dynamic client registration for SIOP
func TestSIOPRegistration(t *testing.T) {
e := echo.New()
t.Run("ValidRegistration", func(t *testing.T) {
registration := map[string]any{
"client_name": "Test SIOP Client",
"client_purpose": "Testing SIOP functionality",
"logo_uri": "https://example.com/logo.png",
}
body, _ := json.Marshal(registration)
req := httptest.NewRequest(http.MethodPost, "/siop/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPRegistration(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]any
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response["client_id"])
assert.Equal(t, "Test SIOP Client", response["client_name"])
assert.Contains(t, response["subject_syntax_types_supported"], "did")
})
t.Run("MissingClientName", func(t *testing.T) {
registration := map[string]any{
"client_purpose": "Testing",
}
body, _ := json.Marshal(registration)
req := httptest.NewRequest(http.MethodPost, "/siop/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPRegistration(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_request", errorResp["error"])
assert.Contains(t, errorResp["error_description"], "Client name is required")
})
}
// TestSIOPMetadata tests SIOP metadata endpoint
func TestSIOPMetadata(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/siop/metadata", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPMetadata(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var metadata map[string]any
err = json.Unmarshal(rec.Body.Bytes(), &metadata)
assert.NoError(t, err)
// Verify required SIOP metadata fields
assert.Equal(t, "https://self-issued.me/v2", metadata["issuer"])
assert.Equal(t, "openid://", metadata["authorization_endpoint"])
assert.Contains(t, metadata["response_types_supported"], "id_token")
assert.Contains(t, metadata["scopes_supported"], "openid")
assert.Contains(t, metadata["subject_types_supported"], "pairwise")
assert.NotNil(t, metadata["vp_formats_supported"])
}
// TestValidateSIOPRequest tests SIOP request validation
func TestValidateSIOPRequest(t *testing.T) {
t.Run("ValidRequest", func(t *testing.T) {
requestURI := "openid://?" +
"response_type=id_token" +
"&client_id=test-client" +
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
"&scope=openid+did" +
"&nonce=test-nonce" +
"&state=test-state"
req, err := ValidateSIOPRequest(requestURI)
assert.NoError(t, err)
assert.NotNil(t, req)
assert.Equal(t, "id_token", req.ResponseType)
assert.Equal(t, "test-client", req.ClientID)
assert.Equal(t, "openid did", req.Scope)
assert.Equal(t, "test-nonce", req.Nonce)
})
t.Run("InvalidURI", func(t *testing.T) {
req, err := ValidateSIOPRequest("not-a-valid-uri")
assert.Error(t, err)
assert.Nil(t, req)
})
t.Run("WithClaims", func(t *testing.T) {
claims := map[string]any{
"id_token": map[string]any{
"email": map[string]any{
"essential": true,
},
"name": nil,
},
}
claimsJSON, _ := json.Marshal(claims)
requestURI := "openid://?" +
"response_type=id_token" +
"&client_id=test-client" +
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
"&scope=openid" +
"&nonce=test-nonce" +
"&claims=" + url.QueryEscape(string(claimsJSON))
req, err := ValidateSIOPRequest(requestURI)
assert.NoError(t, err)
assert.NotNil(t, req)
assert.NotNil(t, req.Claims.IDToken)
})
}
// TestSIOPWithVPToken tests SIOP with Verifiable Presentation
func TestSIOPWithVPToken(t *testing.T) {
e := echo.New()
vpClaims := map[string]any{
"vp_token": map[string]any{
"presentation_definition": map[string]any{
"id": "test-presentation",
"input_descriptors": []map[string]any{
{
"id": "id_credential",
"constraints": map[string]any{
"fields": []map[string]any{
{
"path": []string{"$.type"},
"filter": map[string]any{
"type": "string",
"const": "IdentityCredential",
},
},
},
},
},
},
},
},
}
claimsJSON, _ := json.Marshal(vpClaims)
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("nonce", "test-nonce")
q.Set("claims", string(claimsJSON))
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user_did", "did:sonr:testuser")
// Parse request to set claims
siopReq := &SIOPRequest{
ResponseType: "id_token",
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
Scope: "openid",
Nonce: "test-nonce",
}
err := json.Unmarshal(claimsJSON, &siopReq.Claims)
assert.NoError(t, err)
c.Set("siop_request", siopReq)
err = HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, rec.Code)
location := rec.Header().Get("Location")
assert.Contains(t, location, "id_token=")
// VP token generation is optional, so we don't assert its presence
}
// TestHandleSIOPRequest tests complete SIOP request flow
func TestHandleSIOPRequest(t *testing.T) {
e := echo.New()
t.Run("ValidRequestURI", func(t *testing.T) {
requestURI := "openid://?" +
"response_type=id_token" +
"&client_id=test-client" +
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
"&scope=openid" +
"&nonce=test-nonce"
req := httptest.NewRequest(http.MethodGet, "/siop/request", nil)
q := req.URL.Query()
q.Set("request_uri", requestURI)
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user_did", "did:sonr:testuser")
err := HandleSIOPRequest(c)
assert.NoError(t, err)
// Should process as authorization request
assert.Equal(t, http.StatusFound, rec.Code)
})
t.Run("MissingRequestURI", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/siop/request", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPRequest(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_request", errorResp["error"])
assert.Contains(t, errorResp["error_description"], "Missing request_uri")
})
}
-265
View File
@@ -1,265 +0,0 @@
package handlers
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
// WebAuthnCredential represents a WebAuthn credential
type WebAuthnCredential struct {
CredentialID string `json:"credential_id"`
RawID string `json:"raw_id"`
ClientDataJSON string `json:"client_data_json"`
AttestationObject string `json:"attestation_object"`
Username string `json:"username"`
Origin string `json:"origin"`
PublicKey []byte `json:"public_key"`
Algorithm int32 `json:"algorithm"`
CreatedAt time.Time `json:"created_at"`
}
// WebAuthnRegistrationRequest represents a registration request
type WebAuthnRegistrationRequest struct {
Username string `json:"username"`
Challenge string `json:"challenge,omitempty"`
AutoCreateVault bool `json:"auto_create_vault"`
BroadcastToChain bool `json:"broadcast_to_chain"`
}
// WebAuthnAuthenticationRequest represents an authentication request
type WebAuthnAuthenticationRequest struct {
Username string `json:"username"`
Challenge string `json:"challenge,omitempty"`
}
// WebAuthnRegistrationResponse contains registration ceremony data
type WebAuthnRegistrationResponse struct {
Challenge string `json:"challenge"`
RP WebAuthnRPEntity `json:"rp"`
User WebAuthnUserEntity `json:"user"`
PubKeyCredParams []WebAuthnCredParam `json:"pubKeyCredParams"`
AuthenticatorSelection WebAuthnAuthenticatorSelection `json:"authenticatorSelection"`
Timeout int `json:"timeout"`
Attestation string `json:"attestation"`
}
// WebAuthnAuthenticationResponse contains authentication ceremony data
type WebAuthnAuthenticationResponse struct {
Challenge string `json:"challenge"`
Timeout int `json:"timeout"`
RPID string `json:"rpId"`
AllowCredentials []WebAuthnAllowedCred `json:"allowCredentials"`
UserVerification string `json:"userVerification"`
}
// WebAuthnRPEntity represents relying party info
type WebAuthnRPEntity struct {
ID string `json:"id"`
Name string `json:"name"`
}
// WebAuthnUserEntity represents user info
type WebAuthnUserEntity struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
}
// WebAuthnCredParam represents credential parameters
type WebAuthnCredParam struct {
Type string `json:"type"`
Alg int `json:"alg"`
}
// WebAuthnAuthenticatorSelection represents authenticator requirements
type WebAuthnAuthenticatorSelection struct {
AuthenticatorAttachment string `json:"authenticatorAttachment,omitempty"`
UserVerification string `json:"userVerification"`
ResidentKey string `json:"residentKey,omitempty"`
}
// WebAuthnAllowedCred represents allowed credentials for authentication
type WebAuthnAllowedCred struct {
Type string `json:"type"`
ID string `json:"id"`
}
// OIDCConfig represents OpenID Connect configuration
type OIDCConfig struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
JWKSEndpoint string `json:"jwks_uri"`
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
// OIDCAuthorizationRequest represents an authorization request
type OIDCAuthorizationRequest struct {
ResponseType string `json:"response_type"`
ClientID string `json:"client_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
State string `json:"state"`
Nonce string `json:"nonce,omitempty"`
CodeChallenge string `json:"code_challenge,omitempty"`
CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
}
// OIDCTokenRequest represents a token request
type OIDCTokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
CodeVerifier string `json:"code_verifier,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// OIDCTokenResponse represents a token response
type OIDCTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
Scope string `json:"scope,omitempty"`
}
// OIDCUserInfo represents user information
type OIDCUserInfo struct {
Subject string `json:"sub"`
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
DID string `json:"did,omitempty"`
VaultID string `json:"vault_id,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
Claims map[string]any `json:"claims,omitempty"`
}
// JWKSet represents a JSON Web Key Set
type JWKSet struct {
Keys []JWK `json:"keys"`
}
// JWK represents a JSON Web Key
type JWK struct {
KeyType string `json:"kty"`
Use string `json:"use,omitempty"`
KeyID string `json:"kid"`
Algorithm string `json:"alg,omitempty"`
N string `json:"n,omitempty"` // RSA modulus
E string `json:"e,omitempty"` // RSA exponent
X string `json:"x,omitempty"` // EC x coordinate
Y string `json:"y,omitempty"` // EC y coordinate
Curve string `json:"crv,omitempty"` // EC curve
}
// SIOPRequest represents a Self-Issued OpenID Provider request
type SIOPRequest struct {
ResponseType string `json:"response_type"`
ClientID string `json:"client_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
Nonce string `json:"nonce"`
State string `json:"state,omitempty"`
Claims SIOPClaims `json:"claims,omitempty"`
Registration SIOPRegistration `json:"registration,omitempty"`
}
// SIOPClaims represents claims requested in SIOP
type SIOPClaims struct {
IDToken map[string]ClaimRequest `json:"id_token,omitempty"`
VPToken map[string]ClaimRequest `json:"vp_token,omitempty"`
}
// ClaimRequest represents a claim request
type ClaimRequest struct {
Essential bool `json:"essential,omitempty"`
Value string `json:"value,omitempty"`
Values []string `json:"values,omitempty"`
}
// SIOPRegistration represents client registration in SIOP
type SIOPRegistration struct {
ClientName string `json:"client_name,omitempty"`
ClientPurpose string `json:"client_purpose,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
SubjectSyntaxTypesSupported []string `json:"subject_syntax_types_supported,omitempty"`
VPFormats map[string]any `json:"vp_formats,omitempty"`
}
// SIOPResponse represents a Self-Issued OpenID Provider response
type SIOPResponse struct {
IDToken string `json:"id_token"`
VPToken string `json:"vp_token,omitempty"`
State string `json:"state,omitempty"`
}
// DIDAuthClaims represents DID-based authentication claims
type DIDAuthClaims struct {
jwt.RegisteredClaims
DID string `json:"did"`
Challenge string `json:"challenge,omitempty"`
WebAuthn *WebAuthnCredential `json:"webauthn,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
// AuthorizationCode represents an OAuth2 authorization code
type AuthorizationCode struct {
Code string
ClientID string
RedirectURI string
UserDID string
Scope string
Nonce string
CodeChallenge string
CodeChallengeMethod string
ExpiresAt time.Time
Used bool
}
// OIDCSession represents an active OIDC session
type OIDCSession struct {
SessionID string
UserDID string
ClientID string
Scope string
Nonce string
AccessToken string
RefreshToken string
IDToken string
ExpiresAt time.Time
CreatedAt time.Time
}
// BroadcastRequest represents a blockchain broadcast request
type BroadcastRequest struct {
Message any `json:"message"`
Gasless bool `json:"gasless"`
AutoSign bool `json:"auto_sign"`
FromAddress string `json:"from_address,omitempty"`
}
// BroadcastResponse represents a blockchain broadcast response
type BroadcastResponse struct {
TxHash string `json:"tx_hash"`
Height int64 `json:"height,omitempty"`
Code uint32 `json:"code,omitempty"`
RawLog string `json:"raw_log,omitempty"`
Success bool `json:"success"`
}
-602
View File
@@ -1,602 +0,0 @@
package handlers
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/golang-jwt/jwt/v5"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/did/types"
)
// BlockchainUCANSigner implements UCAN signing with blockchain keys
type BlockchainUCANSigner struct {
didKeeper DIDKeeperInterface
issuerDID string
signingMethod jwt.SigningMethod
privateKey any
}
// DIDKeeperInterface defines the minimal interface needed from DID keeper
type DIDKeeperInterface interface {
GetDIDDocument(ctx context.Context, did string) (*types.DIDDocument, error)
GetVerificationMethod(
ctx context.Context,
did string,
methodID string,
) (*types.VerificationMethod, error)
}
// NewBlockchainUCANSigner creates a new blockchain-integrated UCAN signer
func NewBlockchainUCANSigner(
didKeeper DIDKeeperInterface,
issuerDID string,
) (*BlockchainUCANSigner, error) {
return &BlockchainUCANSigner{
didKeeper: didKeeper,
issuerDID: issuerDID,
signingMethod: jwt.SigningMethodES256, // Default to ES256
}, nil
}
// Sign signs a UCAN token with blockchain keys
func (s *BlockchainUCANSigner) Sign(token *ucan.Token) (string, error) {
// Build JWT claims from UCAN token
claims := s.buildClaims(token)
// Get signing key
signingKey, method, err := s.getSigningKey(context.Background())
if err != nil {
return "", fmt.Errorf("failed to get signing key: %w", err)
}
// Create JWT token
jwtToken := jwt.NewWithClaims(method, claims)
// Sign token
signedToken, err := jwtToken.SignedString(signingKey)
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}
return signedToken, nil
}
// GetIssuerDID returns the issuer DID
func (s *BlockchainUCANSigner) GetIssuerDID() string {
if s.issuerDID == "" {
return "did:sonr:oauth-provider"
}
return s.issuerDID
}
// buildClaims builds JWT claims from UCAN token
func (s *BlockchainUCANSigner) buildClaims(token *ucan.Token) jwt.MapClaims {
claims := jwt.MapClaims{
"iss": token.Issuer,
"aud": token.Audience,
"exp": token.ExpiresAt,
"iat": time.Now().Unix(),
"nbf": token.NotBefore,
}
// Add attenuations
if len(token.Attenuations) > 0 {
attClaims := make([]map[string]any, len(token.Attenuations))
for i, att := range token.Attenuations {
attClaims[i] = s.serializeAttenuation(att)
}
claims["att"] = attClaims
}
// Add proofs if present
if len(token.Proofs) > 0 {
proofClaims := make([]string, len(token.Proofs))
for i, proof := range token.Proofs {
proofClaims[i] = string(proof)
}
claims["prf"] = proofClaims
}
// Add facts if present
if len(token.Facts) > 0 {
factClaims := make([]json.RawMessage, len(token.Facts))
for i, fact := range token.Facts {
factClaims[i] = fact.Data
}
claims["fct"] = factClaims
}
// Add UCAN version
claims["ucv"] = "0.10.0"
return claims
}
// serializeAttenuation serializes an attenuation for JWT claims
func (s *BlockchainUCANSigner) serializeAttenuation(att ucan.Attenuation) map[string]any {
result := map[string]any{
"with": att.Resource.GetURI(),
}
// Handle capability serialization
actions := att.Capability.GetActions()
if len(actions) == 1 {
result["can"] = actions[0]
} else {
result["can"] = actions
}
// Add resource-specific metadata
scheme := att.Resource.GetScheme()
switch scheme {
case "vault":
result["type"] = "vault_operation"
case "service", "svc":
result["type"] = "service_operation"
case "did":
result["type"] = "identity_operation"
case "dwn":
result["type"] = "data_operation"
case "dex", "pool":
result["type"] = "trading_operation"
}
return result
}
// getSigningKey retrieves the signing key from blockchain
func (s *BlockchainUCANSigner) getSigningKey(ctx context.Context) (any, jwt.SigningMethod, error) {
// If we have a cached private key, use it
if s.privateKey != nil {
return s.privateKey, s.signingMethod, nil
}
// For OAuth provider, use a service key
if s.issuerDID == "did:sonr:oauth-provider" || s.issuerDID == "" {
// Generate or retrieve service key
privateKey, publicKey, err := s.generateServiceKey()
if err != nil {
return nil, nil, err
}
s.privateKey = privateKey
s.signingMethod = jwt.SigningMethodEdDSA // Update to EdDSA for Ed25519 keys
_ = publicKey // Store public key if needed
return privateKey, jwt.SigningMethodEdDSA, nil
}
// For DID-based signing, retrieve from DID document
if s.didKeeper != nil {
didDoc, err := s.didKeeper.GetDIDDocument(ctx, s.issuerDID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get DID document: %w", err)
}
// Use the first verification method for signing
if len(didDoc.VerificationMethod) > 0 {
vm := didDoc.VerificationMethod[0]
// Extract key based on verification method kind
switch vm.VerificationMethodKind {
case "Ed25519VerificationKey2020":
// Extract Ed25519 key
privateKey, err := s.extractEd25519Key(vm)
if err != nil {
return nil, nil, err
}
s.privateKey = privateKey
s.signingMethod = jwt.SigningMethodEdDSA
return privateKey, jwt.SigningMethodEdDSA, nil
case "EcdsaSecp256k1VerificationKey2019":
// Extract Secp256k1 key
privateKey, err := s.extractSecp256k1Key(vm)
if err != nil {
return nil, nil, err
}
s.privateKey = privateKey
s.signingMethod = jwt.SigningMethodES256
return privateKey, jwt.SigningMethodES256, nil
default:
return nil, nil, fmt.Errorf(
"unsupported verification method type: %s",
vm.VerificationMethodKind,
)
}
}
}
return nil, nil, fmt.Errorf("no signing key available")
}
// generateServiceKey generates a new service key pair
func (s *BlockchainUCANSigner) generateServiceKey() (ed25519.PrivateKey, ed25519.PublicKey, error) {
// In production, this should retrieve from secure storage
// For now, generate a new key pair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate key pair: %w", err)
}
return privateKey, publicKey, nil
}
// extractEd25519Key extracts Ed25519 private key from verification method
func (s *BlockchainUCANSigner) extractEd25519Key(
vm *types.VerificationMethod,
) (ed25519.PrivateKey, error) {
// In production, this would retrieve the private key from secure storage
// based on the public key in the verification method
// For now, return error as we don't have access to private keys
return nil, fmt.Errorf("private key retrieval not implemented")
}
// extractSecp256k1Key extracts Secp256k1 private key from verification method
func (s *BlockchainUCANSigner) extractSecp256k1Key(
vm *types.VerificationMethod,
) (*secp256k1.PrivKey, error) {
// In production, this would retrieve the private key from secure storage
// based on the public key in the verification method
// For now, return error as we don't have access to private keys
return nil, fmt.Errorf("private key retrieval not implemented")
}
// VerifySignature verifies a UCAN token signature
func (s *BlockchainUCANSigner) VerifySignature(tokenString string) (*ucan.Token, error) {
// Parse token without verification first to get issuer
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims format")
}
issuer, ok := claims["iss"].(string)
if !ok {
return nil, fmt.Errorf("no issuer in token")
}
// Get public key for issuer
publicKey, err := s.getPublicKey(context.Background(), issuer)
if err != nil {
return nil, fmt.Errorf("failed to get public key: %w", err)
}
// Verify token with public key
parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
return publicKey, nil
})
if err != nil {
return nil, fmt.Errorf("token verification failed: %w", err)
}
if !parsedToken.Valid {
return nil, fmt.Errorf("invalid token")
}
// Convert JWT claims to UCAN token
return s.claimsToUCAN(claims, tokenString)
}
// getPublicKey retrieves public key for a DID
func (s *BlockchainUCANSigner) getPublicKey(ctx context.Context, did string) (any, error) {
if did == "did:sonr:oauth-provider" {
// Return service public key
// In production, this would be retrieved from configuration
return []byte("oauth-provider-public-key"), nil
}
if s.didKeeper != nil {
didDoc, err := s.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return nil, fmt.Errorf("failed to get DID document: %w", err)
}
if len(didDoc.VerificationMethod) > 0 {
vm := didDoc.VerificationMethod[0]
// Extract public key based on verification method kind
switch vm.VerificationMethodKind {
case "Ed25519VerificationKey2020":
// Decode base64 public key
publicKeyBytes, err := base64.StdEncoding.DecodeString(vm.PublicKeyMultibase)
if err != nil {
return nil, err
}
return ed25519.PublicKey(publicKeyBytes), nil
case "EcdsaSecp256k1VerificationKey2019":
// Decode and return Secp256k1 public key
publicKeyBytes, err := base64.StdEncoding.DecodeString(vm.PublicKeyMultibase)
if err != nil {
return nil, err
}
return publicKeyBytes, nil
default:
return nil, fmt.Errorf(
"unsupported verification method type: %s",
vm.VerificationMethodKind,
)
}
}
}
return nil, fmt.Errorf("no public key found for DID: %s", did)
}
// claimsToUCAN converts JWT claims to UCAN token
func (s *BlockchainUCANSigner) claimsToUCAN(
claims jwt.MapClaims,
rawToken string,
) (*ucan.Token, error) {
token := &ucan.Token{
Raw: rawToken,
}
// Extract standard claims
if iss, ok := claims["iss"].(string); ok {
token.Issuer = iss
}
if aud, ok := claims["aud"].(string); ok {
token.Audience = aud
}
if exp, ok := claims["exp"].(float64); ok {
token.ExpiresAt = int64(exp)
}
if nbf, ok := claims["nbf"].(float64); ok {
token.NotBefore = int64(nbf)
}
// Extract attenuations
if attClaims, ok := claims["att"].([]any); ok {
attenuations := make([]ucan.Attenuation, 0, len(attClaims))
for _, attItem := range attClaims {
if attMap, ok := attItem.(map[string]any); ok {
att, err := s.parseAttenuation(attMap)
if err != nil {
return nil, fmt.Errorf("failed to parse attenuation: %w", err)
}
attenuations = append(attenuations, att)
}
}
token.Attenuations = attenuations
}
// Extract proofs
if proofClaims, ok := claims["prf"].([]any); ok {
proofs := make([]ucan.Proof, 0, len(proofClaims))
for _, proofItem := range proofClaims {
if proofStr, ok := proofItem.(string); ok {
proofs = append(proofs, ucan.Proof(proofStr))
}
}
token.Proofs = proofs
}
// Extract facts
if factClaims, ok := claims["fct"].([]any); ok {
facts := make([]ucan.Fact, 0, len(factClaims))
for _, factItem := range factClaims {
factData, _ := json.Marshal(factItem)
facts = append(facts, ucan.Fact{
Data: json.RawMessage(factData),
})
}
token.Facts = facts
}
return token, nil
}
// parseAttenuation parses an attenuation from JWT claims
func (s *BlockchainUCANSigner) parseAttenuation(attMap map[string]any) (ucan.Attenuation, error) {
// Extract resource URI
resourceURI, ok := attMap["with"].(string)
if !ok {
return ucan.Attenuation{}, fmt.Errorf("missing resource URI")
}
// Parse resource
resource := &SimpleResource{
Scheme: "generic",
Value: resourceURI,
}
// Extract scheme from URI if possible
if len(resourceURI) > 0 {
for _, scheme := range []string{"vault:", "service:", "did:", "dwn:", "dex:", "pool:"} {
if len(resourceURI) >= len(scheme) && resourceURI[:len(scheme)] == scheme {
resource.Scheme = scheme[:len(scheme)-1]
resource.Value = resourceURI[len(scheme):]
break
}
}
}
// Extract capability
var capability ucan.Capability
switch can := attMap["can"].(type) {
case string:
capability = &ucan.SimpleCapability{Action: can}
case []any:
actions := make([]string, 0, len(can))
for _, action := range can {
if actionStr, ok := action.(string); ok {
actions = append(actions, actionStr)
}
}
capability = &ucan.MultiCapability{Actions: actions}
default:
return ucan.Attenuation{}, fmt.Errorf("invalid capability format")
}
return ucan.Attenuation{
Capability: capability,
Resource: resource,
}, nil
}
// SetPrivateKey sets a private key for signing (for testing)
func (s *BlockchainUCANSigner) SetPrivateKey(privateKey any, method jwt.SigningMethod) {
s.privateKey = privateKey
s.signingMethod = method
}
// CreateDelegationToken creates a UCAN token for delegation
func (s *BlockchainUCANSigner) CreateDelegationToken(
issuer, audience string,
attenuations []ucan.Attenuation,
proofs []ucan.Proof,
expiresIn time.Duration,
) (string, error) {
token := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(expiresIn).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
}
return s.Sign(token)
}
// ValidateDelegationChain validates a chain of UCAN delegations
func (s *BlockchainUCANSigner) ValidateDelegationChain(tokens []string) error {
if len(tokens) == 0 {
return fmt.Errorf("empty delegation chain")
}
var previousToken *ucan.Token
for i, tokenString := range tokens {
// Verify current token
token, err := s.VerifySignature(tokenString)
if err != nil {
return fmt.Errorf("failed to verify token %d: %w", i, err)
}
// Check expiration
if time.Now().Unix() > token.ExpiresAt {
return fmt.Errorf("token %d has expired", i)
}
// Check not before
if token.NotBefore > 0 && time.Now().Unix() < token.NotBefore {
return fmt.Errorf("token %d not yet valid", i)
}
// Validate delegation chain
if previousToken != nil {
// Check that previous token's audience matches current issuer
if previousToken.Audience != token.Issuer {
return fmt.Errorf("broken delegation chain at token %d", i)
}
// Check that capabilities are properly attenuated
if !s.isProperlyAttenuated(previousToken.Attenuations, token.Attenuations) {
return fmt.Errorf("improper attenuation at token %d", i)
}
}
previousToken = token
}
return nil
}
// isProperlyAttenuated checks if child attenuations are properly attenuated from parent
func (s *BlockchainUCANSigner) isProperlyAttenuated(parent, child []ucan.Attenuation) bool {
// Child cannot have more permissions than parent
for _, childAtt := range child {
found := false
for _, parentAtt := range parent {
// Check if child attenuation is covered by parent
if s.attenuationCovers(parentAtt, childAtt) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// attenuationCovers checks if parent attenuation covers child
func (s *BlockchainUCANSigner) attenuationCovers(parent, child ucan.Attenuation) bool {
// Check resource match
if parent.Resource.GetScheme() != child.Resource.GetScheme() {
// Wildcard scheme matches all
if parent.Resource.GetScheme() != "*" {
return false
}
}
// Check capability coverage
parentActions := parent.Capability.GetActions()
childActions := child.Capability.GetActions()
// If parent has wildcard, it covers everything
for _, action := range parentActions {
if action == "*" {
return true
}
}
// Check each child action is in parent
for _, childAction := range childActions {
found := false
for _, parentAction := range parentActions {
if parentAction == childAction {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// RefreshToken creates a new token from an existing one with updated expiration
func (s *BlockchainUCANSigner) RefreshToken(
tokenString string,
newExpiration time.Duration,
) (string, error) {
// Verify existing token
token, err := s.VerifySignature(tokenString)
if err != nil {
return "", fmt.Errorf("failed to verify token: %w", err)
}
// Create new token with same claims but new expiration
newToken := &ucan.Token{
Issuer: token.Issuer,
Audience: token.Audience,
ExpiresAt: time.Now().Add(newExpiration).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: token.Attenuations,
Proofs: append(token.Proofs, ucan.Proof(tokenString)), // Add original as proof
Facts: token.Facts,
}
return s.Sign(newToken)
}
-535
View File
@@ -1,535 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/hibiken/asynq"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/bridge/tasks"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/types/ipfs"
)
// VaultHandlers holds all vault-related handlers and their dependencies
type VaultHandlers struct {
IPFSClient ipfs.IPFSClient
ConnectionManager *ConnectionManager
SSEManager *SSEManager
}
// NewVaultHandlers creates a new VaultHandlers instance
func NewVaultHandlers(
ipfsClient ipfs.IPFSClient,
connManager *ConnectionManager,
sseManager *SSEManager,
) *VaultHandlers {
return &VaultHandlers{
IPFSClient: ipfsClient,
ConnectionManager: connManager,
SSEManager: sseManager,
}
}
// GetQueueFromPriority determines the appropriate queue based on priority
func GetQueueFromPriority(priority string) string {
switch priority {
case "critical", "high":
return "critical"
case "low":
return "low"
default:
return "default"
}
}
// GenerateHandler handles vault generation requests
func (vh *VaultHandlers) GenerateHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
// Extract user info from JWT token
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
userID := claims["user_id"].(string)
var req struct {
UserID int `json:"user_id"`
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
task, err := tasks.NewUCANDIDTask(req.UserID)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
// Broadcast task status to WebSocket and SSE connections
go func() {
message := TaskStatusMessage{
TaskID: info.ID,
Status: "enqueued",
Time: time.Now(),
}
vh.ConnectionManager.BroadcastToTask(info.ID, message)
vh.SSEManager.BroadcastToSSE(info.ID, message)
// Simulate task progression for demonstration
// In a real implementation, this would be triggered by the actual task processors
time.Sleep(1 * time.Second)
processingMessage := TaskStatusMessage{
TaskID: info.ID,
Status: "processing",
Progress: 50,
Time: time.Now(),
}
vh.ConnectionManager.BroadcastToTask(info.ID, processingMessage)
vh.SSEManager.BroadcastToSSE(info.ID, processingMessage)
// Simulate completion
time.Sleep(2 * time.Second)
completedMessage := TaskStatusMessage{
TaskID: info.ID,
Status: "completed",
Progress: 100,
Time: time.Now(),
}
vh.ConnectionManager.BroadcastToTask(info.ID, completedMessage)
vh.SSEManager.BroadcastToSSE(info.ID, completedMessage)
}()
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
"user_id": userID,
})
}
}
// SignHandler handles vault signing requests
func (vh *VaultHandlers) SignHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
Message []byte `json:"message"`
Enclave *mpc.EnclaveData `json:"enclave,omitempty"` // Direct enclave data (fallback)
EnclaveCID string `json:"enclave_cid,omitempty"` // IPFS CID reference
Password []byte `json:"password,omitempty"` // Decryption password for IPFS stored data
UCANToken string `json:"ucan_token,omitempty"` // UCAN token for authorization
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if len(req.Message) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Message is required"})
}
// Handle enclave data - either direct or via IPFS CID
var enclave *mpc.EnclaveData
if req.EnclaveCID != "" {
// Retrieve enclave from IPFS
if vh.IPFSClient == nil {
return c.JSON(
http.StatusServiceUnavailable,
map[string]string{"error": "IPFS client not available"},
)
}
encryptedData, err := vh.IPFSClient.Get(req.EnclaveCID)
if err != nil {
return c.JSON(
http.StatusNotFound,
map[string]string{"error": "Failed to retrieve enclave from IPFS"},
)
}
// Create temporary enclave for decryption
tempEnclave := &mpc.EnclaveData{}
decryptedData, err := tempEnclave.Decrypt(req.Password, encryptedData)
if err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to decrypt enclave data"},
)
}
// Unmarshal decrypted data
enclave = &mpc.EnclaveData{}
if err := enclave.Unmarshal(decryptedData); err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to parse enclave data"},
)
}
} else if req.Enclave != nil {
// Use directly provided enclave data
enclave = req.Enclave
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Either enclave or enclave_cid is required"})
}
task, err := tasks.NewUCANSignTask(
0,
req.Message,
) // TODO: Extract userID from JWT or context
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// VerifyHandler handles vault verification requests
func (vh *VaultHandlers) VerifyHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
PublicKey []byte `json:"public_key"`
Message []byte `json:"message"`
Signature []byte `json:"signature"`
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if len(req.PublicKey) == 0 || len(req.Message) == 0 || len(req.Signature) == 0 {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "PublicKey, message, and signature are required"},
)
}
task, err := tasks.NewUCANVerifyTask(
0,
req.Message,
req.Signature,
) // TODO: Extract userID from JWT or context
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// ExportHandler handles vault export requests
func (vh *VaultHandlers) ExportHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
Enclave *mpc.EnclaveData `json:"enclave,omitempty"` // Direct enclave data
EnclaveCID string `json:"enclave_cid,omitempty"` // IPFS CID reference
Password []byte `json:"password"` // For encryption/decryption
StoreIPFS bool `json:"store_ipfs,omitempty"` // Whether to store result in IPFS
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if len(req.Password) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Password is required"})
}
// Handle enclave data - either direct or via IPFS CID
var enclave *mpc.EnclaveData
if req.EnclaveCID != "" {
// Retrieve enclave from IPFS
if vh.IPFSClient == nil {
return c.JSON(
http.StatusServiceUnavailable,
map[string]string{"error": "IPFS client not available"},
)
}
encryptedData, err := vh.IPFSClient.Get(req.EnclaveCID)
if err != nil {
return c.JSON(
http.StatusNotFound,
map[string]string{"error": "Failed to retrieve enclave from IPFS"},
)
}
// Create temporary enclave for decryption
tempEnclave := &mpc.EnclaveData{}
decryptedData, err := tempEnclave.Decrypt(req.Password, encryptedData)
if err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to decrypt enclave data"},
)
}
// Unmarshal decrypted data
enclave = &mpc.EnclaveData{}
if err := enclave.Unmarshal(decryptedData); err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to parse enclave data"},
)
}
} else if req.Enclave != nil {
// Use directly provided enclave data
enclave = req.Enclave
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Either enclave or enclave_cid is required"})
}
// If store_ipfs is true, encrypt and store the enclave in IPFS first
if req.StoreIPFS && vh.IPFSClient != nil {
encryptedData, err := enclave.Encrypt(req.Password)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to encrypt enclave data"},
)
}
cid, err := vh.IPFSClient.Add(encryptedData)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to store enclave in IPFS"},
)
}
// Return the CID for future reference
return c.JSON(http.StatusOK, map[string]any{
"cid": cid,
"status": "stored",
"message": "Enclave data encrypted and stored in IPFS",
})
}
// Export functionality replaced with UCAN token creation for data access
// Convert export operation to UCAN token generation with export permissions
attenuations := []map[string]any{
{
"can": []string{"export", "read"},
"with": "vault://exported-data",
},
}
task, err := tasks.NewUCANTokenTask(
0,
"did:sonr:export-recipient",
attenuations,
time.Now().Add(24*time.Hour).Unix(),
)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// ImportHandler handles vault import requests
func (vh *VaultHandlers) ImportHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
CID string `json:"cid"`
Password []byte `json:"password"`
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if req.CID == "" || len(req.Password) == 0 {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "CID and password are required"},
)
}
// Import functionality replaced with UCAN token creation for data import
// Convert import operation to UCAN token generation with import permissions
attenuations := []map[string]any{
{
"can": []string{"import", "write"},
"with": fmt.Sprintf("ipfs://%s", req.CID),
},
}
task, err := tasks.NewUCANTokenTask(
0,
"did:sonr:import-recipient",
attenuations,
time.Now().Add(1*time.Hour).Unix(),
)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// RefreshHandler handles vault refresh requests
func (vh *VaultHandlers) RefreshHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
Enclave *mpc.EnclaveData `json:"enclave,omitempty"` // Direct enclave data
EnclaveCID string `json:"enclave_cid,omitempty"` // IPFS CID reference
Password []byte `json:"password,omitempty"` // Decryption password for IPFS stored data
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
// Handle enclave data - either direct or via IPFS CID
var enclave *mpc.EnclaveData
if req.EnclaveCID != "" {
// Retrieve enclave from IPFS
if vh.IPFSClient == nil {
return c.JSON(
http.StatusServiceUnavailable,
map[string]string{"error": "IPFS client not available"},
)
}
encryptedData, err := vh.IPFSClient.Get(req.EnclaveCID)
if err != nil {
return c.JSON(
http.StatusNotFound,
map[string]string{"error": "Failed to retrieve enclave from IPFS"},
)
}
// Create temporary enclave for decryption
tempEnclave := &mpc.EnclaveData{}
decryptedData, err := tempEnclave.Decrypt(req.Password, encryptedData)
if err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to decrypt enclave data"},
)
}
// Unmarshal decrypted data
enclave = &mpc.EnclaveData{}
if err := enclave.Unmarshal(decryptedData); err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to parse enclave data"},
)
}
} else if req.Enclave != nil {
// Use directly provided enclave data
enclave = req.Enclave
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Either enclave or enclave_cid is required"})
}
// Refresh functionality replaced with UCAN DID generation for new identity
// Convert refresh operation to DID generation which includes key refresh
task, err := tasks.NewUCANDIDTask(0) // TODO: Extract userID from JWT or context
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
-558
View File
@@ -1,558 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/types/webauthn"
"github.com/sonr-io/sonr/types/webauthn/webauthncbor"
)
// WebAuthnStore manages WebAuthn sessions and credentials
type WebAuthnStore struct {
mu sync.RWMutex
sessions map[string]*WebAuthnSession
credentials map[string][]*WebAuthnCredential
}
// WebAuthnSession holds session data for WebAuthn ceremonies
type WebAuthnSession struct {
Challenge string
Username string
CreatedAt time.Time
SessionType string // "registration" or "authentication"
}
var (
webAuthnStore = &WebAuthnStore{
sessions: make(map[string]*WebAuthnSession),
credentials: make(map[string][]*WebAuthnCredential),
}
sessionTimeout = 5 * time.Minute
)
// BeginWebAuthnRegistration starts WebAuthn registration ceremony
func BeginWebAuthnRegistration(c echo.Context) error {
var req WebAuthnRegistrationRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
if req.Username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Store the request in context for later use in FinishWebAuthnRegistration
c.Set("webauthn_registration_request", &req)
// Generate challenge
challenge, err := generateChallenge()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate challenge",
})
}
// Store session
session := &WebAuthnSession{
Challenge: challenge,
Username: req.Username,
CreatedAt: time.Now(),
SessionType: "registration",
}
webAuthnStore.mu.Lock()
webAuthnStore.sessions[req.Username] = session
webAuthnStore.mu.Unlock()
// Create registration response
response := WebAuthnRegistrationResponse{
Challenge: challenge,
RP: WebAuthnRPEntity{
ID: "localhost", // TODO: Get from config
Name: "Sonr Identity Platform",
},
User: WebAuthnUserEntity{
ID: base64.URLEncoding.EncodeToString([]byte(req.Username)),
Name: req.Username,
DisplayName: req.Username,
},
PubKeyCredParams: []WebAuthnCredParam{
{Type: "public-key", Alg: -7}, // ES256
{Type: "public-key", Alg: -257}, // RS256
},
AuthenticatorSelection: WebAuthnAuthenticatorSelection{
AuthenticatorAttachment: "platform",
UserVerification: "required",
ResidentKey: "preferred",
},
Timeout: 60000,
Attestation: "direct",
}
return c.JSON(http.StatusOK, response)
}
// FinishWebAuthnRegistration completes WebAuthn registration ceremony
func FinishWebAuthnRegistration(c echo.Context) error {
username := c.QueryParam("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Parse registration response
var regResponse map[string]any
if err := c.Bind(&regResponse); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid registration response",
})
}
// Get stored session
webAuthnStore.mu.RLock()
session, exists := webAuthnStore.sessions[username]
webAuthnStore.mu.RUnlock()
if !exists || session.SessionType != "registration" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No registration session found",
})
}
// Check session timeout
if time.Since(session.CreatedAt) > sessionTimeout {
webAuthnStore.mu.Lock()
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Registration session expired",
})
}
// Extract credential data
credentialID, _ := regResponse["id"].(string)
rawID, _ := regResponse["rawId"].(string)
response, _ := regResponse["response"].(map[string]any)
clientDataJSON, _ := response["clientDataJSON"].(string)
attestationObject, _ := response["attestationObject"].(string)
// Verify client data
if err := verifyClientData(clientDataJSON, session.Challenge, "webauthn.create"); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("Client data verification failed: %v", err),
})
}
// Extract public key from attestation
publicKey, algorithm, err := extractPublicKeyFromAttestation(attestationObject)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": fmt.Sprintf("Failed to extract public key: %v", err),
})
}
// Create credential
credential := &WebAuthnCredential{
CredentialID: credentialID,
RawID: rawID,
ClientDataJSON: clientDataJSON,
AttestationObject: attestationObject,
Username: username,
Origin: "localhost", // TODO: Extract from client data
PublicKey: publicKey,
Algorithm: algorithm,
CreatedAt: time.Now(),
}
// Store credential
webAuthnStore.mu.Lock()
webAuthnStore.credentials[username] = append(webAuthnStore.credentials[username], credential)
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
// Check if we should broadcast to blockchain
broadcastReq, ok := c.Get("broadcast_to_chain").(bool)
if !ok {
// Check from original request stored in context
if origReq, exists := c.Get("webauthn_registration_request").(*WebAuthnRegistrationRequest); exists {
broadcastReq = origReq.BroadcastToChain
}
}
var broadcastResult *BroadcastResponse
if broadcastReq {
// Broadcast WebAuthn credential to blockchain as gasless transaction
result, err := BroadcastWebAuthnRegistration(credential, true)
if err != nil {
// Log error but don't fail registration
c.Logger().Error("Failed to broadcast WebAuthn credential:", err)
} else {
broadcastResult = result
}
}
// Check if we should create a vault
autoCreateVault, ok := c.Get("auto_create_vault").(bool)
if !ok {
// Check from original request
if origReq, exists := c.Get("webauthn_registration_request").(*WebAuthnRegistrationRequest); exists {
autoCreateVault = origReq.AutoCreateVault
}
}
var vaultResult *BroadcastResponse
if autoCreateVault {
// Create vault for the user
vaultConfig := map[string]any{
"type": "standard",
"encryption": "AES256",
"owner": username,
}
userDID := fmt.Sprintf("did:sonr:%s", username)
result, err := BroadcastVaultCreation(userDID, vaultConfig)
if err != nil {
// Log error but don't fail registration
c.Logger().Error("Failed to create vault:", err)
} else {
vaultResult = result
}
}
finalResponse := map[string]any{
"success": true,
"message": "Registration completed successfully",
"credentialId": credentialID,
}
if broadcastResult != nil {
finalResponse["broadcast"] = broadcastResult
}
if vaultResult != nil {
finalResponse["vault"] = vaultResult
}
return c.JSON(http.StatusOK, finalResponse)
}
// BeginWebAuthnAuthentication starts WebAuthn authentication ceremony
func BeginWebAuthnAuthentication(c echo.Context) error {
var req WebAuthnAuthenticationRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
if req.Username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Check if user has credentials
webAuthnStore.mu.RLock()
credentials, exists := webAuthnStore.credentials[req.Username]
webAuthnStore.mu.RUnlock()
if !exists || len(credentials) == 0 {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "No credentials found for user",
})
}
// Generate challenge
challenge, err := generateChallenge()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate challenge",
})
}
// Store session
session := &WebAuthnSession{
Challenge: challenge,
Username: req.Username,
CreatedAt: time.Now(),
SessionType: "authentication",
}
webAuthnStore.mu.Lock()
webAuthnStore.sessions[req.Username] = session
webAuthnStore.mu.Unlock()
// Build allowed credentials
allowCredentials := make([]WebAuthnAllowedCred, len(credentials))
for i, cred := range credentials {
allowCredentials[i] = WebAuthnAllowedCred{
Type: "public-key",
ID: cred.CredentialID,
}
}
// Create authentication response
response := WebAuthnAuthenticationResponse{
Challenge: challenge,
Timeout: 60000,
RPID: "localhost", // TODO: Get from config
AllowCredentials: allowCredentials,
UserVerification: "required",
}
return c.JSON(http.StatusOK, response)
}
// FinishWebAuthnAuthentication completes WebAuthn authentication ceremony
func FinishWebAuthnAuthentication(c echo.Context) error {
username := c.QueryParam("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Parse authentication response
var authResponse map[string]any
if err := c.Bind(&authResponse); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid authentication response",
})
}
// Get stored session
webAuthnStore.mu.RLock()
session, exists := webAuthnStore.sessions[username]
webAuthnStore.mu.RUnlock()
if !exists || session.SessionType != "authentication" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No authentication session found",
})
}
// Check session timeout
if time.Since(session.CreatedAt) > sessionTimeout {
webAuthnStore.mu.Lock()
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Authentication session expired",
})
}
// Extract response data
credentialID, _ := authResponse["id"].(string)
response, _ := authResponse["response"].(map[string]any)
clientDataJSON, _ := response["clientDataJSON"].(string)
authenticatorData, _ := response["authenticatorData"].(string)
signature, _ := response["signature"].(string)
userHandle, _ := response["userHandle"].(string)
// Verify client data
if err := verifyClientData(clientDataJSON, session.Challenge, "webauthn.get"); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("Client data verification failed: %v", err),
})
}
// Find matching credential
webAuthnStore.mu.RLock()
credentials := webAuthnStore.credentials[username]
webAuthnStore.mu.RUnlock()
var matchedCredential *WebAuthnCredential
for _, cred := range credentials {
if cred.CredentialID == credentialID {
matchedCredential = cred
break
}
}
if matchedCredential == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credential",
})
}
// TODO: Verify signature using the stored public key
// This would require implementing proper WebAuthn signature verification
// Clean up session
webAuthnStore.mu.Lock()
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
// Create authenticated session
userDID := fmt.Sprintf("did:sonr:%s", username)
authSession := &OIDCSession{
SessionID: generateSessionID(),
UserDID: userDID,
ClientID: "webauthn-client",
Scope: "openid profile did vault",
AccessToken: generateAccessToken(userDID),
RefreshToken: generateRefreshToken(),
ExpiresAt: time.Now().Add(time.Hour),
CreatedAt: time.Now(),
}
// Store session for OIDC compatibility
oidcProvider.mu.Lock()
oidcProvider.sessions[authSession.AccessToken] = authSession
oidcProvider.mu.Unlock()
// Set user context for downstream handlers
c.Set("user_did", userDID)
c.Set("authenticated", true)
c.Set("auth_method", "webauthn")
c.Set("credential_id", credentialID)
return c.JSON(http.StatusOK, map[string]any{
"success": true,
"message": "Authentication successful",
"credentialId": credentialID,
"accessToken": authSession.AccessToken,
"expiresIn": 3600,
"userDID": userDID,
"sessionId": authSession.SessionID,
"authenticatorData": authenticatorData,
"signature": signature,
"userHandle": userHandle,
})
}
// generateChallenge creates a cryptographically secure challenge
func generateChallenge() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
// verifyClientData verifies client data JSON
func verifyClientData(clientDataJSON, expectedChallenge, expectedType string) error {
clientData, err := webauthn.ValidateClientDataJSONFormat(clientDataJSON)
if err != nil {
return fmt.Errorf("failed to parse client data: %w", err)
}
if clientData.Challenge != expectedChallenge {
return fmt.Errorf("challenge mismatch")
}
if clientData.Type != expectedType {
return fmt.Errorf(
"invalid client data type: expected %s, got %s",
expectedType,
clientData.Type,
)
}
// TODO: Verify origin from config
expectedOrigins := []string{
"http://localhost",
"http://localhost:8080",
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
"http://localhost:8084",
}
validOrigin := false
for _, origin := range expectedOrigins {
if clientData.Origin == origin {
validOrigin = true
break
}
}
if !validOrigin {
return fmt.Errorf("invalid origin: %s", clientData.Origin)
}
return nil
}
// extractPublicKeyFromAttestation extracts public key from attestation object
func extractPublicKeyFromAttestation(attestationObjectB64 string) ([]byte, int32, error) {
// Validate format
if err := webauthn.ValidateAttestationObjectFormat(attestationObjectB64); err != nil {
return nil, 0, fmt.Errorf("invalid attestation format: %w", err)
}
// Decode attestation object
attestationBytes, err := base64.RawURLEncoding.DecodeString(attestationObjectB64)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode attestation: %w", err)
}
// Parse CBOR
var attestationObj webauthn.AttestationObject
if err := webauthncbor.Unmarshal(attestationBytes, &attestationObj); err != nil {
return nil, 0, fmt.Errorf("failed to unmarshal attestation: %w", err)
}
// Unmarshal authenticator data
if err := attestationObj.AuthData.Unmarshal(attestationObj.RawAuthData); err != nil {
return nil, 0, fmt.Errorf("failed to unmarshal auth data: %w", err)
}
// Check for attested credential data
if !attestationObj.AuthData.Flags.HasAttestedCredentialData() {
return nil, 0, fmt.Errorf("no attested credential data")
}
publicKey := attestationObj.AuthData.AttData.CredentialPublicKey
if len(publicKey) == 0 {
return nil, 0, fmt.Errorf("no public key found")
}
// Default to ES256 algorithm
algorithm := int32(-7)
return publicKey, algorithm, nil
}
// GetWebAuthnCredentials retrieves credentials for a user
func GetWebAuthnCredentials(c echo.Context) error {
username := c.Param("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
webAuthnStore.mu.RLock()
credentials, exists := webAuthnStore.credentials[username]
webAuthnStore.mu.RUnlock()
if !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "No credentials found for user",
})
}
// Return sanitized credentials (without sensitive data)
sanitized := make([]map[string]any, len(credentials))
for i, cred := range credentials {
sanitized[i] = map[string]any{
"credentialId": cred.CredentialID,
"createdAt": cred.CreatedAt,
"algorithm": cred.Algorithm,
}
}
return c.JSON(http.StatusOK, sanitized)
}
-284
View File
@@ -1,284 +0,0 @@
package handlers
import (
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
)
// TaskStatusMessage represents a WebSocket message for task status updates
type TaskStatusMessage struct {
TaskID string `json:"task_id"`
Status string `json:"status"`
Progress int `json:"progress,omitempty"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Time time.Time `json:"timestamp"`
}
// ConnectionManager manages WebSocket connections for task status broadcasting
type ConnectionManager struct {
connections map[string]map[*websocket.Conn]bool // taskID -> connections
mutex sync.RWMutex
}
// NewConnectionManager creates a new connection manager
func NewConnectionManager() *ConnectionManager {
return &ConnectionManager{
connections: make(map[string]map[*websocket.Conn]bool),
mutex: sync.RWMutex{},
}
}
// AddConnection adds a WebSocket connection for a specific task ID
func (cm *ConnectionManager) AddConnection(taskID string, conn *websocket.Conn) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cm.connections[taskID] == nil {
cm.connections[taskID] = make(map[*websocket.Conn]bool)
}
cm.connections[taskID][conn] = true
log.Printf("WebSocket connection added for task: %s", taskID)
}
// RemoveConnection removes a WebSocket connection
func (cm *ConnectionManager) RemoveConnection(taskID string, conn *websocket.Conn) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if connections, exists := cm.connections[taskID]; exists {
delete(connections, conn)
if len(connections) == 0 {
delete(cm.connections, taskID)
}
}
log.Printf("WebSocket connection removed for task: %s", taskID)
}
// BroadcastToTask broadcasts a message to all connections listening to a specific task
func (cm *ConnectionManager) BroadcastToTask(taskID string, message TaskStatusMessage) {
cm.mutex.RLock()
connections, exists := cm.connections[taskID]
cm.mutex.RUnlock()
if !exists {
return
}
for conn := range connections {
if err := conn.WriteJSON(message); err != nil {
log.Printf("Error broadcasting to WebSocket: %v", err)
cm.RemoveConnection(taskID, conn)
conn.Close()
}
}
}
// SSEManager manages Server-Sent Event connections for task status streaming
type SSEManager struct {
connections map[string]map[chan TaskStatusMessage]bool // taskID -> channels
mutex sync.RWMutex
}
// NewSSEManager creates a new SSE manager
func NewSSEManager() *SSEManager {
return &SSEManager{
connections: make(map[string]map[chan TaskStatusMessage]bool),
mutex: sync.RWMutex{},
}
}
// AddSSEConnection adds an SSE channel for a specific task ID
func (sm *SSEManager) AddSSEConnection(taskID string, ch chan TaskStatusMessage) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
if sm.connections[taskID] == nil {
sm.connections[taskID] = make(map[chan TaskStatusMessage]bool)
}
sm.connections[taskID][ch] = true
log.Printf("SSE connection added for task: %s", taskID)
}
// RemoveSSEConnection removes an SSE channel
func (sm *SSEManager) RemoveSSEConnection(taskID string, ch chan TaskStatusMessage) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
if channels, exists := sm.connections[taskID]; exists {
delete(channels, ch)
if len(channels) == 0 {
delete(sm.connections, taskID)
}
}
close(ch)
log.Printf("SSE connection removed for task: %s", taskID)
}
// BroadcastToSSE broadcasts a message to all SSE connections listening to a specific task
func (sm *SSEManager) BroadcastToSSE(taskID string, message TaskStatusMessage) {
sm.mutex.RLock()
channels, exists := sm.connections[taskID]
sm.mutex.RUnlock()
if !exists {
return
}
for ch := range channels {
select {
case ch <- message:
// Message sent successfully
default:
// Channel is blocked, remove it
log.Printf("SSE channel blocked, removing for task: %s", taskID)
sm.RemoveSSEConnection(taskID, ch)
}
}
}
// WebSocketHandler handles WebSocket connections for real-time task status updates
func WebSocketHandler(
upgrader *websocket.Upgrader,
connectionManager *ConnectionManager,
) echo.HandlerFunc {
return func(c echo.Context) error {
taskID := c.Param("task_id")
if taskID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Task ID is required"})
}
// Upgrade HTTP connection to WebSocket
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return err
}
defer ws.Close()
// Add connection to manager
connectionManager.AddConnection(taskID, ws)
defer connectionManager.RemoveConnection(taskID, ws)
// Send initial connection confirmation
initialMessage := TaskStatusMessage{
TaskID: taskID,
Status: "connected",
Time: time.Now(),
}
if err := ws.WriteJSON(initialMessage); err != nil {
log.Printf("Error sending initial message: %v", err)
return err
}
// Keep connection alive and handle incoming messages
for {
// Read message from client (ping/pong for keepalive)
_, _, err := ws.ReadMessage()
if err != nil {
log.Printf("WebSocket read error: %v", err)
break
}
// Echo back a pong message to keep connection alive
pongMessage := TaskStatusMessage{
TaskID: taskID,
Status: "pong",
Time: time.Now(),
}
if err := ws.WriteJSON(pongMessage); err != nil {
log.Printf("Error sending pong: %v", err)
break
}
}
return nil
}
}
// SSEHandler handles Server-Sent Events for streaming task status updates
func SSEHandler(sseManager *SSEManager) echo.HandlerFunc {
return func(c echo.Context) error {
taskID := c.Param("task_id")
if taskID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Task ID is required"})
}
// Set SSE headers
c.Response().Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set("Cache-Control", "no-cache")
c.Response().Header().Set("Connection", "keep-alive")
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
c.Response().Header().Set("Access-Control-Allow-Headers", "Cache-Control")
// Create a channel for this SSE connection
messageCh := make(chan TaskStatusMessage, 10)
sseManager.AddSSEConnection(taskID, messageCh)
defer sseManager.RemoveSSEConnection(taskID, messageCh)
// Send initial connection message
initialMessage := TaskStatusMessage{
TaskID: taskID,
Status: "connected",
Time: time.Now(),
}
fmt.Fprintf(
c.Response().Writer,
"data: {\"task_id\":\"%s\",\"status\":\"connected\",\"timestamp\":\"%s\"}\n\n",
taskID,
initialMessage.Time.Format(time.RFC3339),
)
c.Response().Flush()
// Keep connection alive and send messages
for {
select {
case message, ok := <-messageCh:
if !ok {
return nil
}
// Format message as SSE data
sseData := fmt.Sprintf(
"data: {\"task_id\":\"%s\",\"status\":\"%s\",\"progress\":%d,\"timestamp\":\"%s\"}",
message.TaskID,
message.Status,
message.Progress,
message.Time.Format(time.RFC3339),
)
if message.Error != "" {
sseData = fmt.Sprintf(
"data: {\"task_id\":\"%s\",\"status\":\"%s\",\"error\":\"%s\",\"timestamp\":\"%s\"}",
message.TaskID,
message.Status,
message.Error,
message.Time.Format(time.RFC3339),
)
}
fmt.Fprintf(c.Response().Writer, "%s\n\n", sseData)
c.Response().Flush()
case <-c.Request().Context().Done():
// Client disconnected
return nil
case <-time.After(30 * time.Second):
// Send keepalive message every 30 seconds
fmt.Fprintf(
c.Response().Writer,
"data: {\"task_id\":\"%s\",\"status\":\"keepalive\",\"timestamp\":\"%s\"}\n\n",
taskID,
time.Now().Format(time.RFC3339),
)
c.Response().Flush()
}
}
}
}
-62
View File
@@ -1,62 +0,0 @@
package bridge
import (
"log"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/bridge/tasks"
)
// QueueManager handles Asynq server setup and task registration
type QueueManager struct {
server *asynq.Server
mux *asynq.ServeMux
config *Config
}
// NewQueueManager creates a new queue manager with the given configuration
func NewQueueManager(config *Config) *QueueManager {
// Initialize UCAN task processing server with optimized queue configuration
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: config.RedisAddr},
config.AsynqConfig,
)
// Register UCAN-based task handlers
mux := asynq.NewServeMux()
registerTaskHandlers(mux)
return &QueueManager{
server: srv,
mux: mux,
config: config,
}
}
// registerTaskHandlers registers all UCAN-based task handlers
func registerTaskHandlers(mux *asynq.ServeMux) {
// Core UCAN token operations
mux.Handle(tasks.TypeUCANToken, tasks.NewUCANProcessor())
mux.Handle(tasks.TypeUCANAttenuation, tasks.NewUCANAttenuationProcessor())
// MPC-based cryptographic operations
mux.Handle(tasks.TypeUCANSign, tasks.NewUCANSignProcessor())
mux.Handle(tasks.TypeUCANVerify, tasks.NewUCANVerifyProcessor())
// DID operations (replaces both TypeVaultGenerate and TypeVaultRefresh)
// Serves as proxy for future x/did module integration
mux.Handle(tasks.TypeUCANDIDGeneration, tasks.NewUCANDIDProcessor())
log.Printf("UCAN task handlers registered successfully")
}
// Run starts the Asynq server with the registered task handlers
func (qm *QueueManager) Run() error {
log.Printf("Starting Asynq task server with Redis at %s", qm.config.RedisAddr)
return qm.server.Run(qm.mux)
}
// Shutdown gracefully shuts down the Asynq server
func (qm *QueueManager) Shutdown() {
qm.server.Shutdown()
}
-223
View File
@@ -1,223 +0,0 @@
package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"
"github.com/hibiken/asynq"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/crypto/mpc"
)
// MockAsynqClient provides a test double for asynq.Client
type MockAsynqClient struct {
enqueuedTasks []MockTask
}
type MockTask struct {
Type string
Payload []byte
Queue string
}
func (m *MockAsynqClient) Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error) {
mockTask := MockTask{
Type: task.Type(),
Payload: task.Payload(),
Queue: "default",
}
m.enqueuedTasks = append(m.enqueuedTasks, mockTask)
return &asynq.TaskInfo{
ID: "test-task-id",
Type: task.Type(),
Queue: mockTask.Queue,
}, nil
}
func (m *MockAsynqClient) Close() error {
return nil
}
// setupTestEcho creates a test Echo server and mock client for benchmarking
func setupTestEcho() (*echo.Echo, *MockAsynqClient) {
mockClient := &MockAsynqClient{}
config := &Config{
JWTSecret: []byte("test-secret"),
IPFSClient: &MockIPFSClient{},
}
s := NewServer(config)
return s.Echo(), mockClient
}
// BenchmarkHealthCheckHandler measures the performance of the health check endpoint
func BenchmarkHealthCheckHandler(b *testing.B) {
e, _ := setupTestEcho()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("GET", "/health", nil)
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
})
}
// BenchmarkGenerateHandler measures the performance of the generate endpoint
func BenchmarkGenerateHandler(b *testing.B) {
e, client := setupTestEcho()
payload := map[string]any{
"user_id": 123,
"priority": "default",
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("POST", "/vault/generate", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
})
b.StopTimer()
b.Logf("Tasks enqueued: %d", len(client.enqueuedTasks))
}
// BenchmarkSignHandler measures the performance of the sign endpoint
func BenchmarkSignHandler(b *testing.B) {
e, client := setupTestEcho()
payload := map[string]any{
"message": []byte("benchmark test message"),
"enclave": &mpc.EnclaveData{},
"priority": "default",
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("POST", "/vault/sign", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
})
b.StopTimer()
b.Logf("Tasks enqueued: %d", len(client.enqueuedTasks))
}
// BenchmarkMemoryAllocation measures memory allocation patterns
func BenchmarkMemoryAllocation(b *testing.B) {
e, _ := setupTestEcho()
payload := map[string]any{
"user_id": 123,
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
b.ReportAllocs()
for b.Loop() {
req := httptest.NewRequest("POST", "/vault/generate", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
}
b.StopTimer()
runtime.GC()
runtime.ReadMemStats(&m2)
b.Logf("Memory allocated per operation: %d bytes", (m2.TotalAlloc-m1.TotalAlloc)/uint64(b.N))
b.Logf("Total allocations: %d", m2.Mallocs-m1.Mallocs)
}
// BenchmarkLatencyMeasurement measures end-to-end latency
func BenchmarkLatencyMeasurement(b *testing.B) {
e, _ := setupTestEcho()
payload := map[string]any{
"user_id": 123,
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
var totalLatency time.Duration
minLatency := time.Hour
var maxLatency time.Duration
for b.Loop() {
start := time.Now()
req := httptest.NewRequest("POST", "/vault/generate", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
latency := time.Since(start)
totalLatency += latency
if latency < minLatency {
minLatency = latency
}
if latency > maxLatency {
maxLatency = latency
}
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
b.StopTimer()
avgLatency := totalLatency / time.Duration(b.N)
b.Logf("Average latency: %v", avgLatency)
b.Logf("Min latency: %v", minLatency)
b.Logf("Max latency: %v", maxLatency)
}
-118
View File
@@ -1,118 +0,0 @@
// Package server provides the HTTP server for the highway server
package server
import (
"net/http"
"github.com/gorilla/websocket"
"github.com/hibiken/asynq"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/sonr-io/sonr/bridge/handlers"
"github.com/sonr-io/sonr/types/ipfs"
)
const (
DefaultHTTPAddr = ":8080"
)
// Config holds server configuration
type Config struct {
HTTPAddr string
JWTSecret []byte
IPFSClient ipfs.IPFSClient
}
// Server represents the HTTP server
type Server struct {
config *Config
echo *echo.Echo
upgrader *websocket.Upgrader
connectionManager *handlers.ConnectionManager
sseManager *handlers.SSEManager
vaultHandlers *handlers.VaultHandlers
}
// NewServer creates a new server instance
func NewServer(config *Config) *Server {
// WebSocket upgrader with CORS settings
upgrader := &websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins in development
},
}
// Create connection managers
connectionManager := handlers.NewConnectionManager()
sseManager := handlers.NewSSEManager()
// Create vault handlers
vaultHandlers := handlers.NewVaultHandlers(config.IPFSClient, connectionManager, sseManager)
return &Server{
config: config,
echo: echo.New(),
upgrader: upgrader,
connectionManager: connectionManager,
sseManager: sseManager,
vaultHandlers: vaultHandlers,
}
}
// Echo returns the underlying Echo instance for testing
func (s *Server) Echo() *echo.Echo {
return s.echo
}
// Start starts the HTTP server
func (s *Server) Start(client *asynq.Client) error {
s.setupMiddleware()
s.setupRoutes(client)
addr := s.config.HTTPAddr
if addr == "" {
addr = DefaultHTTPAddr
}
return s.echo.Start(addr)
}
// setupMiddleware configures Echo middleware
func (s *Server) setupMiddleware() {
s.echo.Use(middleware.Logger())
s.echo.Use(middleware.Recover())
s.echo.Use(middleware.CORS())
}
// setupRoutes configures all routes
func (s *Server) setupRoutes(client *asynq.Client) {
// Initialize health checker
handlers.InitHealthChecker(client, s.config.IPFSClient)
// Public endpoints (no authentication required)
s.echo.GET("/health", handlers.HealthCheckHandler) // Liveness probe
s.echo.GET("/ready", handlers.ReadinessHandler) // Readiness probe
s.echo.POST("/auth/login", handlers.LoginHandler(s.config.JWTSecret))
// JWT middleware configuration
jwtConfig := echojwt.Config{
SigningKey: s.config.JWTSecret,
SigningMethod: "HS256",
}
// Protected vault endpoints group with JWT middleware
vault := s.echo.Group("/vault")
vault.Use(echojwt.WithConfig(jwtConfig))
vault.POST("/generate", s.vaultHandlers.GenerateHandler(client))
vault.POST("/sign", s.vaultHandlers.SignHandler(client))
vault.POST("/verify", s.vaultHandlers.VerifyHandler(client))
vault.POST("/export", s.vaultHandlers.ExportHandler(client))
vault.POST("/import", s.vaultHandlers.ImportHandler(client))
vault.POST("/refresh", s.vaultHandlers.RefreshHandler(client))
// WebSocket endpoint for real-time task status updates
vault.GET("/ws/:task_id", handlers.WebSocketHandler(s.upgrader, s.connectionManager))
// Server-Sent Events endpoint for task progress streaming
vault.GET("/events/:task_id", handlers.SSEHandler(s.sseManager))
}
-110
View File
@@ -1,110 +0,0 @@
package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/bridge/handlers"
"github.com/sonr-io/sonr/types/ipfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// MockIPFSClient provides a test implementation of IPFSClient for server tests
type MockIPFSClient struct{}
func (m *MockIPFSClient) Add(data []byte) (string, error) { return "mock-cid", nil }
func (m *MockIPFSClient) AddFile(
file ipfs.File,
) (string, error) {
return "mock-file-cid", nil
}
func (m *MockIPFSClient) AddFolder(
folder ipfs.Folder,
) (string, error) {
return "mock-folder-cid", nil
}
func (m *MockIPFSClient) Get(
cid string,
) ([]byte, error) {
return []byte("mock-ipfs-data"), nil
}
func (m *MockIPFSClient) GetFile(cid string) (ipfs.File, error) { return nil, nil }
func (m *MockIPFSClient) GetFolder(cid string) (ipfs.Folder, error) { return nil, nil }
func (m *MockIPFSClient) Pin(cid string, name string) error { return nil }
func (m *MockIPFSClient) Unpin(cid string) error { return nil }
func (m *MockIPFSClient) Exists(cid string) (bool, error) { return true, nil }
func (m *MockIPFSClient) IsPinned(ipns string) (bool, error) { return true, nil }
func (m *MockIPFSClient) Ls(cid string) ([]string, error) {
return []string{"mock-file1", "mock-file2"}, nil
}
func (m *MockIPFSClient) NodeStatus() (*ipfs.NodeStatus, error) {
return &ipfs.NodeStatus{
PeerID: "mock-peer-id",
Version: "mock-version",
PeerType: "kubo",
ConnectedPeers: 3,
}, nil
}
func setupTestServer() *Server {
config := &Config{
JWTSecret: []byte("test-secret"),
IPFSClient: &MockIPFSClient{},
}
s := NewServer(config)
// Setup routes manually for testing since we can't call Start() which would block
e := s.Echo()
// Setup middleware
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return next(c)
}
})
// Setup routes manually (mimicking server.setupRoutes)
e.GET("/health", handlers.HealthCheckHandler)
e.POST("/auth/login", handlers.LoginHandler(config.JWTSecret))
return s
}
func TestHealthCheckHandler(t *testing.T) {
s := setupTestServer()
e := s.Echo()
req, err := http.NewRequest("GET", "/health", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
var response map[string]string
err = json.Unmarshal(rr.Body.Bytes(), &response)
require.NoError(t, err)
// When health checker is not initialized, it returns "starting" status
// This is expected behavior in test environment
assert.Equal(t, "starting", response["status"])
}
func TestVaultHandlersCreation(t *testing.T) {
// Test the vault handlers creation
vaultHandlers := handlers.NewVaultHandlers(
&MockIPFSClient{},
handlers.NewConnectionManager(),
handlers.NewSSEManager(),
)
assert.NotNil(t, vaultHandlers)
}
-156
View File
@@ -1,156 +0,0 @@
// Package tasks provides UCAN-based task processing for token attenuation operations.
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ╭─────────────────────────────────────────────────────────╮
// │ Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANAttenuationProcessor implements asynq.Handler interface for UCAN attenuation operations.
type UCANAttenuationProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANAttenuationProcessor creates a new UCANAttenuationProcessor for the specified actor system.
func NewUCANAttenuationProcessor() *UCANAttenuationProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANAttenuation)
return &UCANAttenuationProcessor{
pid: pid,
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Payload │
// ╰─────────────────────────────────────────────────────────╯
// UCANAttenuationPayload contains parameters for UCAN token attenuation task.
type UCANAttenuationPayload struct {
UserID int `json:"user_id"`
ParentToken string `json:"parent_token"`
AudienceDID string `json:"audience_did"`
Attenuations []map[string]any `json:"attenuations,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
// NewUCANAttenuationTask creates a new UCAN token attenuation task.
func NewUCANAttenuationTask(
userID int,
parentToken string,
audienceDID string,
attenuations []map[string]any,
expiresAt int64,
) (*asynq.Task, error) {
payload, err := json.Marshal(UCANAttenuationPayload{
UserID: userID,
ParentToken: parentToken,
AudienceDID: audienceDID,
Attenuations: attenuations,
ExpiresAt: expiresAt,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANAttenuation, payload), nil
}
// ╭───────────────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────────────╯
// ProcessTask processes the UCAN token attenuation task.
func (processor *UCANAttenuationProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANAttenuationPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create attenuated token request for the actor
request := &plugin.NewAttenuatedTokenRequest{
ParentToken: p.ParentToken,
AudienceDID: p.AudienceDID,
Attenuations: p.Attenuations,
ExpiresAt: p.ExpiresAt,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.UCANTokenResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN token attenuation failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ DID Generation Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANDIDProcessor implements asynq.Handler interface for DID generation operations.
type UCANDIDProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANDIDProcessor creates a new UCANDIDProcessor for the specified actor system.
func NewUCANDIDProcessor() *UCANDIDProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANDIDGeneration)
return &UCANDIDProcessor{
pid: pid,
}
}
// UCANDIDPayload contains parameters for DID generation task.
type UCANDIDPayload struct {
UserID int `json:"user_id"`
}
// NewUCANDIDTask creates a new DID generation task.
func NewUCANDIDTask(userID int) (*asynq.Task, error) {
payload, err := json.Marshal(UCANDIDPayload{
UserID: userID,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANDIDGeneration, payload), nil
}
// ProcessTask processes the DID generation task.
func (processor *UCANDIDProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANDIDPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Request DID generation from the actor
resp, err := system.Root.RequestFuture(processor.pid, &plugin.GetIssuerDIDResponse{}, KRequestTimeout).
Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.GetIssuerDIDResponse:
if resp.Error != "" {
return fmt.Errorf("DID generation failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
-96
View File
@@ -1,96 +0,0 @@
// Package tasks provides UCAN-based task processing for the refactored Motor plugin.
// This package handles asynchronous UCAN token creation, signing operations,
// and DID management tasks using the MPC-based plugin architecture.
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ╭─────────────────────────────────────────────────────────╮
// │ Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANProcessor implements asynq.Handler interface for UCAN operations.
type UCANProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANProcessor creates a new UCANProcessor for the specified actor system.
func NewUCANProcessor() *UCANProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANToken)
return &UCANProcessor{
pid: pid,
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Payload │
// ╰─────────────────────────────────────────────────────────╯
// UCANTokenPayload contains parameters for UCAN token creation task.
type UCANTokenPayload struct {
UserID int `json:"user_id"`
AudienceDID string `json:"audience_did"`
Attenuations []map[string]any `json:"attenuations,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
// NewUCANTokenTask creates a new UCAN token creation task.
func NewUCANTokenTask(
userID int,
audienceDID string,
attenuations []map[string]any,
expiresAt int64,
) (*asynq.Task, error) {
payload, err := json.Marshal(UCANTokenPayload{
UserID: userID,
AudienceDID: audienceDID,
Attenuations: attenuations,
ExpiresAt: expiresAt,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANToken, payload), nil
}
// ╭───────────────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────────────╯
// ProcessTask processes the UCAN token creation task.
func (processor *UCANProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANTokenPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create UCAN token request for the actor
request := &plugin.NewOriginTokenRequest{
AudienceDID: p.AudienceDID,
Attenuations: p.Attenuations,
ExpiresAt: p.ExpiresAt,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.UCANTokenResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN token creation failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
-149
View File
@@ -1,149 +0,0 @@
// Package tasks provides UCAN-based task processing for MPC signing operations.
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ╭─────────────────────────────────────────────────────────╮
// │ Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANSignProcessor implements asynq.Handler interface for UCAN signing operations.
type UCANSignProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANSignProcessor creates a new UCANSignProcessor for the specified actor system.
func NewUCANSignProcessor() *UCANSignProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANSign)
return &UCANSignProcessor{
pid: pid,
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Payload │
// ╰─────────────────────────────────────────────────────────╯
// UCANSignPayload contains parameters for UCAN signing task.
type UCANSignPayload struct {
UserID int `json:"user_id"`
Data []byte `json:"data"`
}
// NewUCANSignTask creates a new UCAN signing task.
func NewUCANSignTask(userID int, data []byte) (*asynq.Task, error) {
payload, err := json.Marshal(UCANSignPayload{
UserID: userID,
Data: data,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANSign, payload), nil
}
// ╭───────────────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────────────╯
// ProcessTask processes the UCAN signing task.
func (processor *UCANSignProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANSignPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create signing request for the actor
request := &plugin.SignDataRequest{
Data: p.Data,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.SignDataResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN signing failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Verification Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANVerifyProcessor implements asynq.Handler interface for UCAN verification operations.
type UCANVerifyProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANVerifyProcessor creates a new UCANVerifyProcessor for the specified actor system.
func NewUCANVerifyProcessor() *UCANVerifyProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANVerify)
return &UCANVerifyProcessor{
pid: pid,
}
}
// UCANVerifyPayload contains parameters for UCAN verification task.
type UCANVerifyPayload struct {
UserID int `json:"user_id"`
Data []byte `json:"data"`
Signature []byte `json:"signature"`
}
// NewUCANVerifyTask creates a new UCAN verification task.
func NewUCANVerifyTask(userID int, data, signature []byte) (*asynq.Task, error) {
payload, err := json.Marshal(UCANVerifyPayload{
UserID: userID,
Data: data,
Signature: signature,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANVerify, payload), nil
}
// ProcessTask processes the UCAN verification task.
func (processor *UCANVerifyProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANVerifyPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create verification request for the actor
request := &plugin.VerifyDataRequest{
Data: p.Data,
Signature: p.Signature,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.VerifyDataResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN verification failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
-425
View File
@@ -1,425 +0,0 @@
package tasks
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// generateTestEnclaveData creates a test enclave data for testing purposes
func generateTestEnclaveData(t *testing.T) *mpc.EnclaveData {
t.Helper()
// Generate a new enclave for testing
enclave, err := mpc.NewEnclave()
require.NoError(t, err, "failed to generate test enclave")
return enclave.GetData()
}
// MockUCANActor for testing UCAN task processors
type MockUCANActor struct {
responses map[string]any
}
func NewMockUCANActor() *MockUCANActor {
return &MockUCANActor{
responses: make(map[string]any),
}
}
// SlowMockUCANActor simulates timeout scenarios
type SlowMockUCANActor struct{}
func (s *SlowMockUCANActor) Receive(c actor.Context) {
switch c.Message().(type) {
case *plugin.NewOriginTokenRequest:
// Simulate slow response (longer than KRequestTimeout)
time.Sleep(KRequestTimeout + time.Second)
c.Respond(&plugin.UCANTokenResponse{})
}
}
func (m *MockUCANActor) Receive(c actor.Context) {
switch c.Message().(type) {
case *actor.Started:
// Actor started
case *plugin.NewOriginTokenRequest:
c.Respond(&plugin.UCANTokenResponse{
Token: "mock-ucan-token",
Issuer: "did:sonr:mock-issuer",
Address: "mock-address",
})
case *plugin.SignDataRequest:
c.Respond(&plugin.SignDataResponse{
Signature: []byte("mock-signature"),
})
case *plugin.VerifyDataRequest:
c.Respond(&plugin.VerifyDataResponse{
Valid: true,
})
case *plugin.NewAttenuatedTokenRequest:
c.Respond(&plugin.UCANTokenResponse{
Token: "mock-attenuated-token",
Issuer: "did:sonr:mock-issuer",
Address: "mock-address",
})
case *plugin.GetIssuerDIDResponse:
c.Respond(&plugin.GetIssuerDIDResponse{
IssuerDID: "did:sonr:mock-issuer",
Address: "mock-address",
ChainCode: "mock-chain-code",
})
default:
c.Respond(&actor.DeadLetterResponse{})
}
}
func TestUCANTokenTask(t *testing.T) {
tests := []struct {
name string
userID int
audienceDID string
attenuations []map[string]any
expiresAt int64
}{
{
name: "valid UCAN token request",
userID: 123,
audienceDID: "did:sonr:audience",
attenuations: []map[string]any{
{"can": []string{"sign"}, "with": "vault://example"},
},
expiresAt: time.Now().Add(24 * time.Hour).Unix(),
},
{
name: "zero user ID",
userID: 0,
audienceDID: "did:sonr:audience",
attenuations: []map[string]any{},
expiresAt: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANTokenTask(tt.userID, tt.audienceDID, tt.attenuations, tt.expiresAt)
require.NoError(t, err)
assert.Equal(t, TypeUCANToken, task.Type())
var payload UCANTokenPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.audienceDID, payload.AudienceDID)
// Handle JSON unmarshaling type conversion ([]string becomes []any)
assert.Equal(t, len(tt.attenuations), len(payload.Attenuations))
for i, expectedAtt := range tt.attenuations {
actualAtt := payload.Attenuations[i]
for key, expectedVal := range expectedAtt {
actualVal, exists := actualAtt[key]
assert.True(t, exists, "key %s should exist", key)
// Handle []string to []any conversion
if expectedSlice, ok := expectedVal.([]string); ok {
actualSlice, ok := actualVal.([]any)
assert.True(t, ok, "expected []any for key %s", key)
assert.Equal(t, len(expectedSlice), len(actualSlice))
for j, expectedItem := range expectedSlice {
assert.Equal(t, expectedItem, actualSlice[j])
}
} else {
assert.Equal(t, expectedVal, actualVal)
}
}
}
assert.Equal(t, tt.expiresAt, payload.ExpiresAt)
})
}
}
func TestUCANSignTask(t *testing.T) {
tests := []struct {
name string
userID int
data []byte
}{
{
name: "valid sign request",
userID: 123,
data: []byte("test data to sign"),
},
{
name: "empty data",
userID: 456,
data: []byte{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANSignTask(tt.userID, tt.data)
require.NoError(t, err)
assert.Equal(t, TypeUCANSign, task.Type())
var payload UCANSignPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.data, payload.Data)
})
}
}
func TestUCANVerifyTask(t *testing.T) {
tests := []struct {
name string
userID int
data []byte
signature []byte
}{
{
name: "valid verify request",
userID: 123,
data: []byte("test data to verify"),
signature: []byte("test-signature"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANVerifyTask(tt.userID, tt.data, tt.signature)
require.NoError(t, err)
assert.Equal(t, TypeUCANVerify, task.Type())
var payload UCANVerifyPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.data, payload.Data)
assert.Equal(t, tt.signature, payload.Signature)
})
}
}
func TestUCANAttenuationTask(t *testing.T) {
tests := []struct {
name string
userID int
parentToken string
audienceDID string
attenuations []map[string]any
expiresAt int64
}{
{
name: "valid attenuation request",
userID: 123,
parentToken: "parent-ucan-token",
audienceDID: "did:sonr:delegated",
attenuations: []map[string]any{
{"can": []string{"read"}, "with": "vault://example"},
},
expiresAt: time.Now().Add(1 * time.Hour).Unix(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANAttenuationTask(
tt.userID,
tt.parentToken,
tt.audienceDID,
tt.attenuations,
tt.expiresAt,
)
require.NoError(t, err)
assert.Equal(t, TypeUCANAttenuation, task.Type())
var payload UCANAttenuationPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.parentToken, payload.ParentToken)
assert.Equal(t, tt.audienceDID, payload.AudienceDID)
// Handle JSON unmarshaling type conversion ([]string becomes []any)
assert.Equal(t, len(tt.attenuations), len(payload.Attenuations))
for i, expectedAtt := range tt.attenuations {
actualAtt := payload.Attenuations[i]
for key, expectedVal := range expectedAtt {
actualVal, exists := actualAtt[key]
assert.True(t, exists, "key %s should exist", key)
// Handle []string to []any conversion
if expectedSlice, ok := expectedVal.([]string); ok {
actualSlice, ok := actualVal.([]any)
assert.True(t, ok, "expected []any for key %s", key)
assert.Equal(t, len(expectedSlice), len(actualSlice))
for j, expectedItem := range expectedSlice {
assert.Equal(t, expectedItem, actualSlice[j])
}
} else {
assert.Equal(t, expectedVal, actualVal)
}
}
}
assert.Equal(t, tt.expiresAt, payload.ExpiresAt)
})
}
}
func TestUCANDIDTask(t *testing.T) {
tests := []struct {
name string
userID int
}{
{"valid DID request", 123},
{"zero user ID", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANDIDTask(tt.userID)
require.NoError(t, err)
assert.Equal(t, TypeUCANDIDGeneration, task.Type())
var payload UCANDIDPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
})
}
}
// Test processor functionality with mock actors
func TestUCANTokenProcessor(t *testing.T) {
// Create actor system
system := actor.NewActorSystem()
defer system.Shutdown()
// Create mock actor - use the mock instead of real plugin
mockActor := system.Root.Spawn(actor.PropsFromProducer(func() actor.Actor {
return NewMockUCANActor()
}))
// Create processor with mock actor
processor := &UCANProcessor{pid: mockActor}
// Create test task
task, err := NewUCANTokenTask(123, "did:sonr:audience", []map[string]any{
{"can": []string{"sign"}, "with": "vault://example"},
}, time.Now().Add(24*time.Hour).Unix())
require.NoError(t, err)
// Process the task - the error is expected because the mock actor returns a dead letter
// The processor should handle this gracefully
err = processor.ProcessTask(context.Background(), task)
// For now, we expect this to fail with the dead letter error
// In a real scenario, the actor would be properly initialized with enclave data
assert.Error(t, err, "expected error due to mock actor limitations")
assert.Contains(t, err.Error(), "dead letter", "should get dead letter error from mock")
}
// TestUCANActorWithRealEnclave tests the real UCAN actor with generated enclave data
func TestUCANActorWithRealEnclave(t *testing.T) {
// Skip this test if we're not in integration test mode
if testing.Short() {
t.Skip("skipping integration test")
}
// Skip this test in CI environments where WASM plugin is not available
if os.Getenv("CI") == "true" || os.Getenv("GITHUB_ACTIONS") == "true" {
t.Skip("skipping WASM enclave test in CI environment")
}
// Generate test enclave data
enclaveData := generateTestEnclaveData(t)
// Create enclave config with test data
config := plugin.DefaultEnclaveConfig()
config.EnclaveData = enclaveData
// Create actor system
system := actor.NewActorSystem()
defer system.Shutdown()
// Create real UCAN actor with the enclave configuration
realActor := system.Root.Spawn(plugin.PropsWithConfig(config))
// Wait for actor to initialize (longer wait for CI environments)
time.Sleep(500 * time.Millisecond)
// Test creating a UCAN token through the actor
req := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:test-audience",
Attenuations: []map[string]any{
{"can": []string{"read"}, "with": "vault://test"},
},
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
// Send request to actor with enhanced error handling and longer timeout for CI
result := system.Root.RequestFuture(realActor, req, 10*time.Second)
response, err := result.Result()
if err != nil {
t.Logf("Actor request failed: %v", err)
// If it's a timeout or WASM-related error, that's expected in test environments
if err.Error() == "future: timeout" || strings.Contains(err.Error(), "wasm") {
t.Skip("skipping test due to WASM enclave not available in test environment")
}
}
// The response could be an error if the actor didn't initialize properly
// For now, just verify we got some response (could be error or success) - but only if not a timeout
if err == nil || err.Error() != "future: timeout" {
assert.NotNil(t, response, "should receive some response from actor")
}
// Check if this is a WASM-related error (expected when WASM plugin is not available)
if err == nil && response != nil {
// The response could be a string or an error
responseStr := ""
if errResponse, ok := response.(error); ok {
responseStr = errResponse.Error()
} else if strResponse, ok := response.(string); ok {
responseStr = strResponse
} else {
responseStr = fmt.Sprintf("%+v", response)
}
if responseStr != "" && (strings.Contains(responseStr, "wasm error: unreachable") ||
strings.Contains(responseStr, "wasm not available") ||
strings.Contains(responseStr, "runtime.notInitialized")) {
t.Log("WASM plugin not available (expected in test environment)")
} else {
t.Logf("Actor successfully processed request: %+v", response)
}
} else {
t.Logf("Actor response: %+v, error: %v", response, err)
}
}
func TestInvalidJSONPayload(t *testing.T) {
// Create processor
processor := NewUCANProcessor()
// Create task with invalid JSON
task := asynq.NewTask(TypeUCANToken, []byte("invalid json"))
// Process the task - should fail with skip retry
err := processor.ProcessTask(context.Background(), task)
assert.Error(t, err)
assert.Contains(t, err.Error(), "json.Unmarshal failed")
}
-22
View File
@@ -1,22 +0,0 @@
// Package tasks provides UCAN-based tasks for the MPC enclave service.
package tasks
import (
"time"
"github.com/asynkron/protoactor-go/actor"
)
var system = actor.NewActorSystem()
const KRequestTimeout = 20 * time.Second
// A list of UCAN-based task types.
const (
TypeUCANToken = "ucan:token" // Create UCAN origin tokens
TypeUCANAttenuation = "ucan:attenuation" // Create attenuated UCAN tokens
TypeUCANSign = "ucan:sign" // MPC-based data signing
TypeUCANVerify = "ucan:verify" // MPC-based signature verification
TypeUCANDIDGeneration = "ucan:did:generation" // DID generation from MPC enclave
TypeUCANTokenValidation = "ucan:token:validation" // UCAN token validation
)
-101
View File
@@ -1,101 +0,0 @@
package bridge
import (
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/types/ipfs"
)
// MockIPFSClient provides a test implementation of IPFSClient
type MockIPFSClient struct{}
func (m *MockIPFSClient) Add(data []byte) (string, error) {
return "mock-cid", nil
}
func (m *MockIPFSClient) AddFile(file ipfs.File) (string, error) {
return "mock-file-cid", nil
}
func (m *MockIPFSClient) AddFolder(folder ipfs.Folder) (string, error) {
return "mock-folder-cid", nil
}
func (m *MockIPFSClient) Get(cid string) ([]byte, error) {
return []byte("mock-ipfs-data"), nil
}
func (m *MockIPFSClient) GetFile(cid string) (ipfs.File, error) {
return nil, nil
}
func (m *MockIPFSClient) GetFolder(cid string) (ipfs.Folder, error) {
return nil, nil
}
func (m *MockIPFSClient) Pin(cid string, name string) error {
return nil
}
func (m *MockIPFSClient) Unpin(cid string) error {
return nil
}
func (m *MockIPFSClient) Exists(cid string) (bool, error) {
return true, nil
}
func (m *MockIPFSClient) IsPinned(ipns string) (bool, error) {
return true, nil
}
func (m *MockIPFSClient) Ls(cid string) ([]string, error) {
return []string{"mock-file1", "mock-file2"}, nil
}
func (m *MockIPFSClient) NodeStatus() (*ipfs.NodeStatus, error) {
return &ipfs.NodeStatus{
PeerID: "mock-peer-id",
Version: "mock-version",
PeerType: "kubo",
ConnectedPeers: 5,
}, nil
}
// AsynqClientInterface defines the interface we need for testing
type AsynqClientInterface interface {
Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error)
Close() error
}
// MockAsynqClient provides a test double for asynq.Client
type MockAsynqClient struct {
enqueuedTasks []MockTask
}
type MockTask struct {
Type string
Payload []byte
Queue string
}
func (m *MockAsynqClient) Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error) {
mockTask := MockTask{
Type: task.Type(),
Payload: task.Payload(),
Queue: "default", // Default queue
}
// For testing purposes, we'll just use the default queue
// In real implementation, options would be parsed properly
m.enqueuedTasks = append(m.enqueuedTasks, mockTask)
return &asynq.TaskInfo{
ID: "test-task-id",
Type: task.Type(),
Queue: mockTask.Queue,
}, nil
}
func (m *MockAsynqClient) Close() error {
return nil
}
+3 -3
View File
@@ -13,9 +13,9 @@ import (
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/keys"
"github.com/sonr-io/sonr/types/webauthn"
"github.com/sonr-io/sonr/types/webauthn/webauthncbor"
"github.com/sonr-io/sonr/types/webauthn/webauthncose"
"github.com/sonr-io/common/webauthn"
"github.com/sonr-io/common/webauthn/webauthncbor"
"github.com/sonr-io/common/webauthn/webauthncose"
)
// WebAuthnClient provides an interface for WebAuthn operations with Sonr's Decentralized Abstracted Smart Wallets.
+2 -2
View File
@@ -11,7 +11,7 @@ replace (
cosmossdk.io/store => github.com/evmos/cosmos-sdk/store v0.0.0-20240718141609-414cbd051fbe
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
github.com/cosmos/cosmos-sdk => github.com/strangelove-ventures/cosmos-sdk v0.0.0-20250317212103-0767f8c5b1e5
github.com/sonr-io/sonr/crypto => ../crypto
github.com/sonr-io/crypto => ../crypto
github.com/spf13/viper => github.com/spf13/viper v1.17.0
nhooyr.io/websocket => nhooyr.io/websocket v1.8.17
)
@@ -172,7 +172,7 @@ require (
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/sonr-io/sonr/crypto v0.0.0-00010101000000-000000000000 // indirect
github.com/sonr-io/crypto v0.0.0-00010101000000-000000000000 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
-18
View File
@@ -1,18 +0,0 @@
[tool.commitizen]
name = "cz_customize"
tag_format = "hway/v$version"
ignored_tag_formats = ["*/v${version}", "v${version}"]
version_scheme = "semver"
version_provider = "scm"
update_changelog_on_bump = true
changelog_file = "CHANGELOG.md"
major_version_zero = true
annotated_tag = true
pre_bump_hooks = ["bash scripts/hook-bump-pre.sh"]
post_bump_hooks = ["goreleaser release --clean -f cmd/hway/.goreleaser.yml"]
[tool.commitizen.customize]
bump_pattern = "^(feat|fix|refactor|perf|BREAKING CHANGE)"
bump_map = { "BREAKING CHANGE" = "MAJOR", "feat" = "MINOR", "fix" = "PATCH", "refactor" = "PATCH", "perf" = "PATCH" }
default_bump = "PATCH"
changelog_pattern = "^(feat|fix|refactor|docs|build)\\(hway\\)(!)?:"
-293
View File
@@ -1,293 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
---
version: 2
dist: dist/hway
monorepo:
tag_prefix: hway/
dir: cmd/hway
project_name: hway
before:
hooks:
- go mod download
builds:
# Hway Darwin AMD64
- id: hway-darwin-amd64
main: .
binary: hway
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
goos:
- darwin
goarch:
- amd64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X main.Version={{.Version}}
- -X main.Commit={{.Commit}}
- -s -w # Strip debug info
# Hway Darwin ARM64
- id: hway-darwin-arm64
main: .
binary: hway
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=oa64-clang
- CXX=oa64-clang++
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X main.Version={{.Version}}
- -X main.Commit={{.Commit}}
- -s -w # Strip debug info
# Hway Linux AMD64
- id: hway-linux-amd64
main: .
binary: hway
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++
goos:
- linux
goarch:
- amd64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X main.Version={{.Version}}
- -X main.Commit={{.Commit}}
- -s -w
# Hway Linux ARM64
- id: hway-linux-arm64
main: .
binary: hway
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
goos:
- linux
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X main.Version={{.Version}}
- -X main.Commit={{.Commit}}
- -s -w
aur_sources:
- name: hway
disable: true
homepage: "https://sonr.io"
description: "Highway service - bridge between the Public Internet and Sonr blockchain"
maintainers:
- "Sonr <support@sonr.io>"
license: "GPL-3.0"
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://[email protected]/hway.git"
skip_upload: auto
provides:
- hway
conflicts:
- hway-bin
depends:
- glibc
- redis
optdepends:
- "docker: for container-based vault operations"
makedepends:
- go
- git
- make
commit_msg_template: "Update to {{ .Tag }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
prepare: |-
cd "${pkgname}_${pkgver}"
go mod download
build: |-
cd "${pkgname}_${pkgver}"
export CGO_ENABLED=1
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw"
go build \
-ldflags="-w -s -buildid='' -linkmode=external \
-X main.Version=${pkgver}" \
-o hway ./cmd/hway
chmod +x ./hway
package: |-
cd "${pkgname}_${pkgver}"
# bin
install -Dm755 "./hway" "${pkgdir}/usr/bin/hway"
# license
if [ -f "./LICENSE" ]; then
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/hway/LICENSE"
fi
# readme
if [ -f "./README.md" ]; then
install -Dm644 "./README.md" "${pkgdir}/usr/share/doc/hway/README.md"
fi
nix:
- name: hway
ids:
- hway
homepage: "https://sonr.io"
description: "Highway network component for Sonr"
license: "gpl3"
path: pkgs/hway/default.nix
commit_msg_template: "hway: {{ .Tag }}"
dependencies:
- stdenv
- glibc
extra_install: |-
wrapProgram $out/bin/hway --prefix PATH : ${lib.makeBinPath [ glibc stdenv.cc.cc.lib ]}
repository:
owner: sonr-io
name: nur
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
archives:
- id: hway
ids:
- hway-linux-amd64
- hway-linux-arm64
- hway-darwin-amd64
- hway-darwin-arm64
name_template: >-
hway_{{ .Os }}_{{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }}
formats: ["tar.gz"]
files:
- src: README*
wrap_in_directory: false
homebrew_casks:
- name: hway
ids:
- hway
homepage: "https://sonr.io"
description: "Highway service - bridge between the Public Internet and Sonr blockchain"
commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"
directory: Casks
repository:
owner: sonr-io
name: homebrew-tap
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/hway"]
end
nfpms:
- id: hway
package_name: hway
ids:
- hway-linux-amd64
- hway-linux-arm64
file_name_template: "hway_{{ .Os }}_{{ .Arch }}{{ .ConventionalExtension }}"
vendor: Sonr
homepage: "https://sonr.io"
maintainer: "Sonr <support@sonr.io>"
description: "Hway is the bridge between the Public Internet and the Sonr blockchain."
license: "GPL-3.0"
formats:
- rpm
- deb
- apk
- archlinux
contents:
- src: README*
dst: /usr/share/doc/hway
bindir: /usr/bin
section: net
priority: optional
blobs:
- provider: s3
endpoint: https://eb37925850388bca807b7fab964c12bb.r2.cloudflarestorage.com
bucket: releases
region: auto
directory: "hway/{{ .Tag }}"
ids:
- hway
release:
disable: false
github:
owner: sonr-io
name: sonr
name_template: "{{.ProjectName}}/{{ .Tag }}"
draft: false
replace_existing_draft: false # Don't replace drafts
replace_existing_artifacts: false # Append, don't replace
mode: append # Explicitly set to append mode
checksum:
name_template: 'hway_checksums.txt'
npms:
- name: "@sonr.io/hway"
ids:
- hway
description: "Highway service - bridge between the Public Internet and Sonr blockchain"
homepage: "https://sonr.io"
license: "GPL-3.0"
author: "Sonr <support@sonr.io>"
repository: "https://github.com/sonr-io/sonr"
bugs: "https://github.com/sonr-io/sonr/issues"
keywords:
- highway
- bridge
- blockchain
- sonr
- service
access: public
format: tar.gz
snapshot:
version_template: "{{ incpatch .Version }}-dev"
# Changelog configuration
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
-5
View File
@@ -1,5 +0,0 @@
## hway/v0.0.3 (2025-10-04)
## hway/v0.0.2 (2025-10-04)
## hway/v0.0.1 (2025-10-03)
-77
View File
@@ -1,77 +0,0 @@
# Use build argument for base image to allow using pre-cached base
ARG BASE_IMAGE=golang:1.24.7-alpine3.22
# --------------------------------------------------------
# Highway service build stage
FROM ${BASE_IMAGE} AS builder
SHELL ["/bin/sh", "-ecuxo", "pipefail"]
# Install build dependencies
RUN apk add --no-cache \
ca-certificates \
build-base \
git \
linux-headers \
bash
WORKDIR /code
# Copy entire source code
COPY . .
# Fix git ownership issue
RUN git config --global --add safe.directory /code
# Download Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
# Build Highway binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev"); \
COMMIT=$(git log -1 --format='%H' 2>/dev/null || echo "unknown"); \
CGO_ENABLED=1 GOOS=linux \
go build \
-mod=readonly \
-ldflags "-X main.Version=${VERSION} \
-X main.Commit=${COMMIT} \
-w -s" \
-buildvcs=false \
-trimpath \
-o /code/build/hway \
./cmd/hway
# --------------------------------------------------------
# Highway runtime image
FROM alpine:3.17
LABEL org.opencontainers.image.title="Sonr Highway Service"
LABEL org.opencontainers.image.source="https://github.com/sonr-io/sonr"
# Copy binary from builder
COPY --from=builder /code/build/hway /usr/bin/hway
# Install runtime dependencies
RUN apk add --no-cache ca-certificates wget
# Create non-root user
RUN adduser -D -u 1000 highway
# Set working directory
WORKDIR /home/highway
# Switch to non-root user
USER highway
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --spider -q http://localhost:8090/health || exit 1
# Expose Highway port
EXPOSE 8090
# Set default command
ENTRYPOINT ["/usr/bin/hway"]
-110
View File
@@ -1,110 +0,0 @@
#!/usr/bin/make -f
# Version and build information
GIT_ROOT := $(shell git rev-parse --show-toplevel)
VERSION := $(shell echo $(shell git describe --tags 2>/dev/null || echo "dev") | sed 's/^v//')
COMMIT := $(shell git log -1 --format='%H')
BUMP_LEVEL := PATCH
# Build configuration
BUILD_FLAGS := -trimpath
# Linker flags for version info
ldflags = -X main.Version=$(VERSION) \
-X main.Commit=$(COMMIT) \
-s -w
BUILD_FLAGS += -ldflags '$(ldflags)'
HWAY_OUT := $(GIT_ROOT)/build/hway
.PHONY: all build install clean test version help docker
all: build
build:
ifeq ($(OS),Windows_NT)
$(error hway server not supported on Windows)
else
@echo "Building Highway service..."
@go build -mod=readonly $(BUILD_FLAGS) -o $(HWAY_OUT) .
@echo "Binary built: ../../build/hway"
endif
install:
ifeq ($(OS),Windows_NT)
$(error hway server not supported on Windows)
else
@echo "Installing Highway service..."
@go install -mod=readonly $(BUILD_FLAGS) .
@echo "Binary installed to $(GOPATH)/bin/hway"
endif
tidy:
@echo "Tidying Go module..."
@go mod tidy
clean:
@echo "Cleaning build artifacts..."
@rm -f $(HWAY_OUT)
@echo "Clean complete"
test:
@echo "Running Highway service tests..."
@go test -v -race ./...
release:
@echo "Creating hway release..."
@cd $(GIT_ROOT) && cz --config cmd/hway/.cz.toml --no-raise 6,21 bump --yes --increment PATCH
snapshot:
@echo "Dry-Run Bumping Highway version..."
@cd $(GIT_ROOT) && cz --config cmd/hway/.cz.toml bump --yes --no-verify --dry-run --increment PATCH
@echo "Creating hway snapshots for all platforms..."
@cd $(GIT_ROOT) && goreleaser release --snapshot --clean -f cmd/hway/.goreleaser.yml
# Docker build targets
docker:
@echo "Building Highway Docker image..."
@docker build -f Dockerfile -t onsonr/hway:latest -t ghcr.io/sonr-io/hway:latest ../..
@echo "Docker image built and tagged:"
@echo " - onsonr/hway:latest"
@echo " - ghcr.io/sonr-io/hway:latest"
docker-local:
@echo "Building Highway Docker image with local context..."
@docker build -f Dockerfile -t onsonr/hway:local -t ghcr.io/sonr-io/hway:local ../..
@echo "Docker image built and tagged:"
@echo " - onsonr/hway:local"
@echo " - ghcr.io/sonr-io/hway:local"
version:
@echo "Highway Service"
@echo "==============="
@echo "Version: $(VERSION)"
@echo "Commit: $(COMMIT)"
help:
@echo "Highway Service Makefile"
@echo "========================"
@echo ""
@echo "Highway is an Asynq-based task processing service for vault operations,"
@echo "using Redis-backed job queues and Proto.Actor framework for concurrency."
@echo ""
@echo "Available targets:"
@echo " build - Build Highway binary (default)"
@echo " install - Install Highway to GOPATH/bin"
@echo " docker - Build Docker image (onsonr/hway:latest)"
@echo " clean - Remove build artifacts"
@echo " test - Run Highway tests"
@echo " version - Display version information"
@echo " help - Show this help message"
@echo ""
@echo "Requirements:"
@echo " - Redis server running on 127.0.0.1:6379"
@echo " - IPFS nodes for vault operations"
@echo ""
@echo "Examples:"
@echo " make build # Build the Highway service"
@echo " make test # Run tests"
@echo " make install # Install to GOPATH/bin"
Binary file not shown.
-312
View File
@@ -1,312 +0,0 @@
module hway
go 1.24.7
// overrides
replace (
cosmossdk.io/core => cosmossdk.io/core v0.11.0
cosmossdk.io/store => github.com/evmos/cosmos-sdk/store v0.0.0-20240718141609-414cbd051fbe
github.com/CosmWasm/wasmd => github.com/rollchains/wasmd v0.50.0-evm
github.com/cosmos/cosmos-sdk => github.com/strangelove-ventures/cosmos-sdk v0.0.0-20250317212103-0767f8c5b1e5
github.com/cosmos/evm => github.com/strangelove-ventures/cosmos-evm v0.2.0
github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc2
github.com/sonr-io/sonr => ../../
github.com/sonr-io/sonr/crypto => ../../crypto
github.com/spf13/viper => github.com/spf13/viper v1.17.0 // v1.18+ breaks app overrides
nhooyr.io/websocket => nhooyr.io/websocket v1.8.17
)
replace (
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
// Fix btcec version for evmos compatibility
github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.2
// dgrijalva/jwt-go is deprecated and doesn't receive security updates.
// See: https://github.com/cosmos/cosmos-sdk/issues/13134
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
// Fix upstream GHSA-h395-qcrw-5vmq vulnerability.
// See: https://github.com/cosmos/cosmos-sdk/issues/10409
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.8.1
// pin version! 126854af5e6d has issues with the store so that queries fail
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
)
require github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000
require (
cosmossdk.io/api v0.7.6 // indirect
cosmossdk.io/collections v0.4.0 // indirect
cosmossdk.io/core v0.12.0 // indirect
cosmossdk.io/depinject v1.1.0 // indirect
cosmossdk.io/errors v1.0.1 // indirect
cosmossdk.io/math v1.5.0 // indirect
cosmossdk.io/orm v1.0.0-beta.3 // indirect
cosmossdk.io/store v1.1.1 // indirect
cosmossdk.io/x/tx v0.13.7 // indirect
github.com/Oudwins/zog v0.21.6 // indirect
github.com/asynkron/protoactor-go v0.0.0-20240822202345-3c0e61ca19c9 // indirect
github.com/biter777/countries v1.7.5 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/gogoproto v1.7.0 // indirect
github.com/extism/go-sdk v1.7.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/ipfs/boxo v0.32.0 // indirect
github.com/ipfs/kubo v0.35.0 // indirect
github.com/labstack/echo/v4 v4.13.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/sonr-io/sonr/crypto v0.0.0-00010101000000-000000000000 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
require (
cosmossdk.io/log v1.5.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/Workiva/go-datastructures v1.1.3 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/bwesterb/go-ristretto v1.2.3 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/caddyserver/certmagic v0.21.6 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/cockroachdb/crlib v0.0.0-20241015224233-894974b3ad94 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v1.1.2 // indirect
github.com/cockroachdb/pebble/v2 v2.0.3 // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/swiss v0.0.0-20250624142022-d6e517c1d961 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/cometbft/cometbft v0.38.17 // indirect
github.com/cometbft/cometbft-db v0.14.1 // indirect
github.com/consensys/gnark-crypto v0.19.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.1.1 // indirect
github.com/cosmos/cosmos-sdk v0.53.4 // indirect
github.com/cosmos/ics23/go v0.11.0 // indirect
github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/gammazero/deque v1.0.0 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.3 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/hibiken/asynq v0.25.1 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-bitfield v1.1.0 // indirect
github.com/ipfs/go-block-format v0.2.1 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/ipfs/go-datastore v0.8.2 // indirect
github.com/ipfs/go-ds-measure v0.2.2 // indirect
github.com/ipfs/go-fs-lock v0.1.1 // indirect
github.com/ipfs/go-ipfs-cmds v0.14.1 // indirect
github.com/ipfs/go-ipld-cbor v0.2.0 // indirect
github.com/ipfs/go-ipld-format v0.6.1 // indirect
github.com/ipfs/go-ipld-legacy v0.2.1 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.6.0 // indirect
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
github.com/ipfs/go-unixfsnode v1.10.1 // indirect
github.com/ipld/go-car/v2 v2.14.3 // indirect
github.com/ipld/go-codec-dagpb v1.7.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/ipshipyard/p2p-forge v0.5.1 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.0.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/echo-jwt/v4 v4.3.1 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
github.com/libp2p/go-libp2p v0.43.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.33.1 // indirect
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/linxGnu/grocksdb v1.9.8 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0 // indirect
github.com/lmittmann/tint v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // 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-multiaddr v0.16.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.1 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.1 // indirect
github.com/multiformats/go-varint v0.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/orcaman/concurrent-map v1.0.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.19 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.6 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pion/webrtc/v4 v4.1.2 // 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/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/quic-go/webtransport-go v0.9.0 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect
github.com/whyrusleeping/cbor-gen v0.1.2 // indirect
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/dig v1.19.0 // indirect
go.uber.org/fx v1.24.0 // indirect
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
)
-1382
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -1,20 +0,0 @@
// Package main provides the Highway service, a UCAN-based task processing service
// that acts as a bridge proxy for MPC operations and decentralized identity management.
//
// The service processes asynchronous UCAN (User-Controlled Authorization Networks) tasks
// including token creation, delegation, signing, verification, and DID generation.
// It serves as a proxy between the bridge handlers and the underlying blockchain operations.
package main
import (
"github.com/sonr-io/sonr/bridge"
)
func main() {
// Create and configure the Highway service
service := bridge.NewHighwayService()
defer service.Shutdown()
// Start the service and block until shutdown
service.Start()
}
-4
View File
@@ -1,4 +0,0 @@
package main
// Version is set by commitizen during release process
var Version = "dev"
-18
View File
@@ -1,18 +0,0 @@
[tool.commitizen]
name = "cz_customize"
tag_format = "motr/v$version"
ignored_tag_formats = ["*/v${version}", "v${version}"]
version_scheme = "semver"
version_provider = "scm"
update_changelog_on_bump = true
changelog_file = "CHANGELOG.md"
major_version_zero = true
annotated_tag = true
pre_bump_hooks = ["bash scripts/hook-bump-pre.sh"]
post_bump_hooks = ["goreleaser release --clean -f cmd/motr/.goreleaser.yml"]
[tool.commitizen.customize]
bump_pattern = "^(feat|fix|refactor|perf|BREAKING CHANGE)"
bump_map = { "BREAKING CHANGE" = "MAJOR", "feat" = "MINOR", "fix" = "PATCH", "refactor" = "PATCH", "perf" = "PATCH" }
default_bump = "PATCH"
changelog_pattern = "^(feat|fix|refactor|docs|build)\\(motr\\)(!)?:"
-75
View File
@@ -1,75 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
---
version: 2
dist: dist/motr
monorepo:
tag_prefix: motr/
dir: cmd/motr
project_name: motr
before:
hooks:
- go mod download
builds:
- id: motr-wasm
main: .
binary: motr
no_unique_dist_dir: true
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- js
goarch:
- wasm
flags:
- -mod=readonly
- -trimpath
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
hooks:
post:
- cp dist/motr/motr.wasm packages/es/src/worker/app.wasm
archives:
- id: motr-wasm-archive
name_template: "motr_wasm_{{ .Version }}"
formats: ["binary"]
wrap_in_directory: true
blobs:
- provider: s3
endpoint: https://eb37925850388bca807b7fab964c12bb.r2.cloudflarestorage.com
bucket: releases
region: auto
directory: "motr/{{ .Tag }}"
release:
disable: false
github:
owner: sonr-io
name: sonr
name_template: "{{.ProjectName}}/{{ .Tag }}"
draft: false
replace_existing_draft: false # Don't replace drafts
replace_existing_artifacts: false # Append, don't replace
mode: append # Explicitly set to append mode
checksum:
name_template: "motr_checksums.txt"
snapshot:
version_template: "{{ incpatch .Version }}-dev"
# Changelog configuration
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
-5
View File
@@ -1,5 +0,0 @@
## motr/v0.0.3 (2025-10-04)
## motr/v0.0.2 (2025-10-04)
## motr/v0.0.1 (2025-10-03)
-154
View File
@@ -1,154 +0,0 @@
#!/usr/bin/make -f
# Output configuration - outputs to ES package for bundling
GIT_ROOT := $(shell git rev-parse --show-toplevel)
ES_PACKAGE_DIR := $(GIT_ROOT)/packages/es/src/worker
WASM_FILE := app.wasm
JS_FILE := wasm_exec.js
OUTPUT_PATH := $(ES_PACKAGE_DIR)/$(WASM_FILE)
JS_PATH := $(ES_PACKAGE_DIR)/$(JS_FILE)
# Build configuration for WASM
GOOS := js
GOARCH := wasm
CGO_ENABLED := 0
# Version information
VERSION := $(shell echo $(shell git describe --tags 2>/dev/null || echo "dev") | sed 's/^v//')
COMMIT := $(shell git log -1 --format='%H')
# Go installation paths
GOROOT := $(shell go env GOROOT)
WASM_EXEC_SOURCE := $(GOROOT)/misc/wasm/wasm_exec.js
# Build flags
LDFLAGS := -s -w
BUILD_FLAGS := -ldflags="$(LDFLAGS)" -trimpath
.PHONY: all build clean test verify help version runtime tidy
all: build
build: clean-output runtime
@echo "Building Motor WASM module for ES package..."
@echo "Target: $(OUTPUT_PATH)"
@mkdir -p $(ES_PACKAGE_DIR)
@GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_FLAGS) -o $(OUTPUT_PATH) .
@echo "✅ Motor WASM module built successfully"
@echo "Output: $(OUTPUT_PATH)"
@ls -lh $(OUTPUT_PATH) | awk '{print "Size: " $$5}'
@$(MAKE) runtime
runtime:
@echo "Copying WASM runtime..."
@if [ -f "$(WASM_EXEC_SOURCE)" ]; then \
cp "$(WASM_EXEC_SOURCE)" "$(JS_PATH)"; \
echo "✅ WASM runtime copied to $(JS_PATH)"; \
else \
echo "⚠️ WASM runtime not found at $(WASM_EXEC_SOURCE)"; \
echo "You may need to manually copy wasm_exec.js"; \
fi
clean-output:
@echo "Cleaning previous builds..."
@rm -f $(OUTPUT_PATH)
@rm -f $(JS_PATH)
@mkdir -p $(ES_PACKAGE_DIR)
clean:
@echo "Cleaning build artifacts..."
@rm -f $(OUTPUT_PATH)
@rm -f $(JS_PATH)
@echo "✅ Clean complete"
release:
@echo "Creating motr release..."
@cd $(GIT_ROOT) && cz --config cmd/motr/.cz.toml --no-raise 6,21 bump --yes --increment PATCH
snapshot:
@echo "Dry-Run Bumping Motor version..."
@cd $(GIT_ROOT) && cz --config cmd/motr/.cz.toml bump --yes --no-verify --dry-run --increment PATCH
@echo "Creating motr snapshots for all platforms..."
@cd $(GIT_ROOT) && goreleaser release --snapshot --clean -f cmd/motr/.goreleaser.yml
tidy:
@echo "Tidying Motor module..."
@go mod tidy
@echo "✅ Tidy complete"
test:
@echo "Running Motor tests..."
@go test -v ./...
verify: build
@echo "Verifying WASM module..."
@if [ -f "$(OUTPUT_PATH)" ]; then \
file "$(OUTPUT_PATH)"; \
echo "✅ WASM file exists"; \
else \
echo "❌ WASM file not found"; \
exit 1; \
fi
@if [ -f "$(JS_PATH)" ]; then \
echo "✅ Runtime file exists"; \
else \
echo "⚠️ Runtime file not found"; \
fi
@if command -v wasm-validate >/dev/null 2>&1; then \
if wasm-validate "$(OUTPUT_PATH)"; then \
echo "✅ WASM module is valid"; \
else \
echo "❌ WASM module validation failed"; \
exit 1; \
fi \
else \
echo "⚠️ wasm-validate not available, skipping validation"; \
fi
@echo "✅ Verification complete"
@echo ""
@echo "The WASM module has been built in the ES package at:"
@echo " $(OUTPUT_PATH)"
@echo ""
@echo "The ES package will bundle and distribute this via jsDelivr"
version:
@echo "Motor WASM Service Worker"
@echo "========================="
@echo "Version: $(VERSION)"
@echo "Commit: $(COMMIT)"
@echo "Target OS: $(GOOS)"
@echo "Target Arch: $(GOARCH)"
@echo "Output: $(OUTPUT_PATH)"
help:
@echo "Motor WASM Module Makefile"
@echo "=========================="
@echo ""
@echo "Motor provides WebAssembly-based DWN and Wallet operations"
@echo "for the @sonr.io/es package to distribute via jsDelivr."
@echo ""
@echo "Available targets:"
@echo " build - Build Motor WASM module (default)"
@echo " clean - Remove all build artifacts"
@echo " test - Run Motor tests"
@echo " tidy - Tidy Go module dependencies"
@echo " verify - Build and validate WASM module"
@echo " version - Display version information"
@echo " help - Show this help message"
@echo ""
@echo "Build components (called by build):"
@echo " runtime - Copy WASM runtime (wasm_exec.js)"
@echo ""
@echo "Output location:"
@echo " ES Package: $(ES_PACKAGE_DIR)/"
@echo " WASM File: $(OUTPUT_PATH)"
@echo ""
@echo "Integration:"
@echo " The WASM module is built directly into the ES package plugins"
@echo " directory for bundling and CDN distribution. The TypeScript"
@echo " client in @sonr.io/es/plugins/motor handles service worker management."
@echo ""
@echo "Examples:"
@echo " make build # Build WASM module into ES package"
@echo " make verify # Build and validate the module"
@echo " make clean # Remove artifacts"
-385
View File
@@ -1,385 +0,0 @@
# Motor WASM Service Worker - Payment Gateway & OIDC Authorization
Motor is a WebAssembly-based HTTP server that runs as a Service Worker in the browser, providing secure payment processing and OpenID Connect (OIDC) authorization without requiring backend infrastructure.
## Overview
Motor implements a comprehensive payment gateway and identity provider that runs entirely in the browser:
1. **Payment Gateway**: W3C Payment Handler API compliant payment processing with PCI DSS compliance
2. **OIDC Authorization**: Complete OpenID Connect provider with JWT token management
3. **Service Worker**: Runs as a browser service worker using go-wasm-http-server
## Features
### Payment Gateway (W3C Payment Handler API)
- ✅ Process payment transactions securely
- ✅ PCI DSS compliant card tokenization
- ✅ Card validation (Luhn algorithm, CVV, expiry)
- ✅ Transaction signing with HMAC-SHA256
- ✅ AES-256-GCM encryption for sensitive data
- ✅ Payment method validation
- ✅ Refund processing
- ✅ Comprehensive audit logging
### OIDC Authorization
- ✅ Discovery endpoint (`.well-known/openid-configuration`)
- ✅ Authorization endpoint with PKCE support
- ✅ Token endpoint with JWT generation
- ✅ UserInfo endpoint
- ✅ JWKS endpoint for key rotation
- ✅ RS256 JWT signing
- ✅ Refresh token support
### Security Features
- ✅ Rate limiting (100 requests/minute per client)
- ✅ Origin validation
- ✅ Security headers (CSP, X-Frame-Options, etc.)
- ✅ CORS configuration
- ✅ Secure token generation
- ✅ Card number masking
- ✅ Sensitive data sanitization
## API Endpoints
### Payment Gateway Endpoints
#### Process Payment
```http
POST /api/payment/process
Content-Type: application/json
{
"method": "card",
"amount": 100.00,
"currency": "USD",
"card_number": "4111111111111111",
"cvv": "123",
"expiry_month": 12,
"expiry_year": 2025,
"billing_address": {
"line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
}
}
```
#### Validate Payment Method
```http
POST /api/payment/validate
Content-Type: application/json
{
"method": "card",
"card_number": "4111111111111111",
"cvv": "123",
"expiry_month": 12,
"expiry_year": 2025
}
```
#### Get Payment Status
```http
GET /api/payment/status/:id
```
#### Process Refund
```http
POST /api/payment/refund
Content-Type: application/json
{
"payment_id": "pay_abc123",
"amount": 50.00,
"reason": "Customer request"
}
```
#### W3C Payment Handler API
```http
GET /payment/instruments
POST /payment/canmakepayment
POST /payment/paymentrequest
```
### OIDC Endpoints
#### Discovery
```http
GET /.well-known/openid-configuration
```
#### Authorization
```http
GET /authorize?client_id=CLIENT_ID&redirect_uri=URI&response_type=code&scope=openid%20profile
```
#### Token Exchange
```http
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTH_CODE&client_id=CLIENT_ID
```
#### UserInfo
```http
GET /userinfo
Authorization: Bearer ACCESS_TOKEN
```
#### JWKS
```http
GET /.well-known/jwks.json
```
### Health & Monitoring
```http
GET /health
GET /status
```
## Building
### Using Make
```bash
# Build Motor WASM module
make motr-wasm
# Or build directly
cd cmd/motr
GOOS=js GOARCH=wasm go build -o ../../packages/es/src/plugins/motor/motor.wasm .
```
### Build Output
The WASM module is built to: `packages/es/src/plugins/motor/motor.wasm`
## Integration
### Service Worker Registration
```javascript
// motor-worker.js
importScripts('https://cdn.jsdelivr.net/gh/golang/go@go1.23.4/misc/wasm/wasm_exec.js');
importScripts('https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v2.2.1/sw.js');
// Register Motor WASM as HTTP listener
registerWasmHTTPListener('motor.wasm', {
base: '/api'
});
```
### TypeScript Client Usage
```typescript
import { PaymentGatewayClient, OIDCClient } from '@sonr.io/es/plugins/motor';
// Initialize clients
const payment = new PaymentGatewayClient('https://localhost:3000');
const oidc = new OIDCClient('https://localhost:3000');
// Process a payment
const result = await payment.processPayment({
method: 'card',
amount: 100.00,
currency: 'USD',
card_number: '4111111111111111',
cvv: '123',
expiry_month: 12,
expiry_year: 2025
});
// OIDC authorization flow
const authUrl = await oidc.buildAuthorizationUrl({
client_id: 'my-app',
redirect_uri: 'https://myapp.com/callback',
scope: 'openid profile email'
});
// Exchange authorization code for tokens
const tokens = await oidc.exchangeCode('auth_code_here', 'code_verifier');
```
## Security Implementation
### PCI DSS Compliance
- **Tokenization**: Cards are immediately tokenized, raw data never stored
- **Encryption**: AES-256-GCM for all sensitive data at rest
- **Masking**: Card numbers always masked except last 4 digits
- **Audit Logging**: Complete audit trail for compliance
- **CVV Handling**: CVV never stored, only validated
### Transaction Security
- **Signing**: HMAC-SHA256 signatures on all transactions
- **Verification**: Signature verification before processing
- **Tamper Detection**: Any modification invalidates transaction
- **Idempotency**: Duplicate transaction prevention
### Authentication Security
- **JWT Signing**: RS256 with 2048-bit RSA keys
- **PKCE**: Proof Key for Code Exchange for authorization flow
- **Token Expiration**: Configurable expiration (default 1 hour)
- **Refresh Tokens**: Secure refresh token rotation
## Testing
### Unit Tests
```bash
# Run unit tests (without WASM constraints)
go test ./cmd/motr/...
```
### Integration Tests
```bash
# Build WASM first
make motr-wasm
# Run integration tests
cd cmd/motr
GOOS=js GOARCH=wasm go test -v
```
### Test Coverage
- ✅ Payment processing flows
- ✅ Card validation (Luhn, CVV, expiry)
- ✅ Tokenization and encryption
- ✅ Transaction signing/verification
- ✅ OIDC discovery and flows
- ✅ JWT generation/validation
- ✅ Rate limiting
- ✅ Security headers
- ✅ PCI compliance features
## Performance
### Bundle Size
- WASM module: ~3-4MB (production build)
- Service Worker: ~10KB
- TypeScript client: ~25KB (minified)
### Optimization
- Built with `-ldflags="-s -w"` for size reduction
- Gzip compression reduces transfer to ~1MB
- Lazy loading recommended for optimal performance
### Benchmarks
- Payment processing: <100ms average
- Token generation: <50ms
- Card validation: <10ms
- Encryption/decryption: <20ms
## Browser Compatibility
| Feature | Chrome | Firefox | Safari | Edge |
|---------|--------|---------|--------|------|
| Service Workers | 45+ | 44+ | 11.1+ | 17+ |
| WebAssembly | 57+ | 52+ | 11+ | 16+ |
| Payment Handler | 68+ | - | - | 79+ |
| Full Support | 68+ | 52+* | 11.1+* | 79+ |
*Payment Handler API has limited support
## Configuration
### Environment Variables
```javascript
// Configure in service worker
const config = {
issuer: 'https://motor.sonr.io',
rateLimit: 100, // requests per minute
rateWindow: 60000, // milliseconds
tokenExpiry: 3600, // seconds
allowedOrigins: ['https://localhost:3000']
};
```
### Security Settings
- Rate limiting: Configurable per-client limits
- CORS: Configurable allowed origins
- CSP: Customizable content security policy
- Token expiry: Adjustable for different use cases
## Development
### Prerequisites
- Go 1.21+ (1.23+ recommended)
- Modern browser with Service Worker support
- HTTPS or localhost (Service Workers requirement)
### Local Development
```bash
# Build WASM module
make motr-wasm
# Start local server (example)
cd packages/es/src/plugins/motor
python3 -m http.server 8080 --bind localhost
# Access at https://localhost:8080
```
### Debugging
- Browser DevTools: Network tab for API inspection
- Service Worker: Application tab for SW debugging
- Console: WASM logs and errors
- Payment Handler: chrome://settings/content/paymentHandler
## Production Deployment
### Best Practices
1. **HTTPS Required**: Service Workers only work over HTTPS
2. **Cache Strategy**: Implement proper cache headers
3. **Error Handling**: Comprehensive error logging
4. **Monitoring**: Track payment success rates
5. **Compliance**: Regular PCI DSS audits
### Deployment Checklist
- [ ] Configure production issuer URL
- [ ] Set appropriate rate limits
- [ ] Configure allowed origins
- [ ] Enable production encryption keys
- [ ] Set up monitoring and alerting
- [ ] Configure backup payment processors
- [ ] Implement fraud detection rules
- [ ] Schedule security audits
## Troubleshooting
### Common Issues
#### Service Worker Not Registering
- Ensure HTTPS or localhost
- Check browser compatibility
- Verify WASM file path
#### Payment Processing Errors
- Validate card details format
- Check rate limiting
- Verify origin is allowed
#### OIDC Flow Issues
- Ensure redirect URI matches
- Check PKCE implementation
- Verify token expiration
### Debug Mode
Enable debug logging in the service worker:
```javascript
// motor-worker.js
const DEBUG = true;
```
## License
This implementation is part of the Sonr project and follows the same license terms.
## Support
For issues, questions, or contributions:
- GitHub Issues: https://github.com/sonr-io/sonr/issues
- Documentation: https://docs.sonr.io
- Security: security@sonr.io (for security vulnerabilities)
-10
View File
@@ -1,10 +0,0 @@
module motr
go 1.24.7
require github.com/go-sonr/wasm-http-server/v3 v3.0.0
require (
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/nlepage/go-js-promise v1.0.0 // indirect
)
-6
View File
@@ -1,6 +0,0 @@
github.com/go-sonr/wasm-http-server/v3 v3.0.0 h1:DY/XaJD0jfKlvpVlOTqyU5b+SRVOulR5+zOye0LK0o0=
github.com/go-sonr/wasm-http-server/v3 v3.0.0/go.mod h1:97QCYR5OlAEWeKeeIKCMZqCOIHqJakyTIFu0sbwDSJ8=
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/nlepage/go-js-promise v1.0.0 h1:K7OmJ3+0BgWJ2LfXchg2sI6RDr7AW/KWR8182epFwGQ=
github.com/nlepage/go-js-promise v1.0.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo=
-446
View File
@@ -1,446 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"encoding/json"
"net/http"
"strings"
"time"
)
// Health & Status Handlers
// handleHealth returns service health status
func handleHealth(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "healthy",
"service": "motor-gateway",
"timestamp": time.Now().Unix(),
})
}
// handleStatus returns detailed service status
func handleStatus(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "operational",
"version": "1.0.0",
"services": map[string]string{
"payment_gateway": "active",
"oidc_provider": "active",
},
"uptime": time.Now().Unix(),
})
}
// W3C Payment Handler API Handlers
// handlePaymentInstruments returns available payment instruments
func handlePaymentInstruments(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "GET" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
instruments := paymentHandler.GetInstruments()
writeJSON(w, http.StatusOK, map[string]interface{}{
"instruments": instruments,
})
}
// handleCanMakePayment checks if payment can be made
func handleCanMakePayment(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req struct {
MethodData []PaymentMethod `json:"methodData"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
canMakePayment := paymentHandler.CanMakePayment(req.MethodData)
writeJSON(w, http.StatusOK, map[string]interface{}{
"canMakePayment": canMakePayment,
})
}
// handlePaymentRequest handles W3C PaymentRequestEvent
func handlePaymentRequest(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Parse payment request event
var reqData json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
paymentReq, err := SerializePaymentRequest(reqData)
if err != nil {
writeError(w, http.StatusBadRequest, "Invalid payment request")
return
}
// Process payment request
tx, err := paymentHandler.ProcessPayment(paymentReq)
if err != nil {
writeError(w, http.StatusInternalServerError, "Payment processing failed")
return
}
// Return payment response
if tx.Response != nil {
writeJSON(w, http.StatusOK, tx.Response)
} else {
writeJSON(w, http.StatusAccepted, map[string]interface{}{
"transactionId": tx.ID,
"status": tx.Status,
})
}
}
// Payment Gateway Handlers
// handlePaymentProcess processes a payment transaction using W3C Payment Handler API
func handlePaymentProcess(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Parse payment request
var reqData json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
paymentReq, err := SerializePaymentRequest(reqData)
if err != nil {
writeError(w, http.StatusBadRequest, "Invalid payment request")
return
}
// Process payment
tx, err := paymentHandler.ProcessPayment(paymentReq)
if err != nil {
writeError(w, http.StatusInternalServerError, "Payment processing failed")
return
}
writeJSON(w, http.StatusOK, tx)
}
// handlePaymentValidate validates a payment method
func handlePaymentValidate(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Parse validation request
var req struct {
Method string `json:"method"`
Data map[string]interface{} `json:"data"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
// Validate payment method
valid, err := paymentHandler.ValidatePaymentMethod(req.Method, req.Data)
if err != nil {
writeError(w, http.StatusInternalServerError, "Validation failed")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"valid": valid,
"method": req.Method,
"message": "Payment method validation complete",
})
}
// handlePaymentStatus returns payment transaction status
func handlePaymentStatus(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "GET" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Extract transaction ID from path
txID := strings.TrimPrefix(r.URL.Path, "/api/payment/status/")
if txID == "" {
writeError(w, http.StatusBadRequest, "Transaction ID required")
return
}
// Get transaction from handler
tx, exists := paymentHandler.GetTransaction(txID)
if !exists {
writeError(w, http.StatusNotFound, "Transaction not found")
return
}
writeJSON(w, http.StatusOK, tx)
}
// handlePaymentRefund processes a refund
func handlePaymentRefund(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// TODO: Implement refund processing
writeJSON(w, http.StatusOK, map[string]interface{}{
"refund_id": "ref_" + generateID(),
"status": "processing",
"message": "Refund initiated",
})
}
// OIDC Handlers
// handleOIDCDiscovery returns OIDC discovery document
func handleOIDCDiscovery(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
discovery := oidcProvider.GetDiscovery()
writeJSON(w, http.StatusOK, discovery)
}
// handleJWKS returns JSON Web Key Set
func handleJWKS(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
jwk := jwtManager.GetPublicKeyJWK()
writeJSON(w, http.StatusOK, map[string]interface{}{
"keys": []map[string]interface{}{jwk},
})
}
// handleAuthorize handles authorization requests
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "GET" && r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Parse authorization request
clientID := r.FormValue("client_id")
redirectURI := r.FormValue("redirect_uri")
responseType := r.FormValue("response_type")
scope := r.FormValue("scope")
state := r.FormValue("state")
nonce := r.FormValue("nonce")
codeChallenge := r.FormValue("code_challenge")
codeChallengeMethod := r.FormValue("code_challenge_method")
// Validate request
if clientID == "" || redirectURI == "" || responseType == "" {
writeError(w, http.StatusBadRequest, "Missing required parameters")
return
}
// For demo, auto-approve with test user
userID := "test-user"
// Generate authorization code
authCode, err := oidcProvider.GenerateAuthorizationCode(
clientID, redirectURI, scope, state, nonce, userID,
codeChallenge, codeChallengeMethod,
)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
// Return authorization code
writeJSON(w, http.StatusOK, map[string]interface{}{
"code": authCode.Code,
"state": state,
"redirect_uri": redirectURI,
})
}
// handleToken handles token requests
func handleToken(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Parse token request
var req TokenRequest
req.GrantType = r.FormValue("grant_type")
req.Code = r.FormValue("code")
req.RedirectURI = r.FormValue("redirect_uri")
req.ClientID = r.FormValue("client_id")
req.ClientSecret = r.FormValue("client_secret")
req.RefreshToken = r.FormValue("refresh_token")
req.Scope = r.FormValue("scope")
req.CodeVerifier = r.FormValue("code_verifier")
// Handle based on grant type
var resp *TokenResponse
var err error
switch req.GrantType {
case "authorization_code":
resp, err = oidcProvider.ExchangeCode(&req)
case "refresh_token":
// TODO: Implement refresh token flow
writeError(w, http.StatusNotImplemented, "Refresh token not yet implemented")
return
default:
writeError(w, http.StatusBadRequest, "Unsupported grant type")
return
}
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, resp)
}
// handleUserInfo returns user information
func handleUserInfo(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
handleCORS(w)
return
}
if r.Method != "GET" && r.Method != "POST" {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Get bearer token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
writeError(w, http.StatusUnauthorized, "Missing authorization header")
return
}
// Extract token
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
writeError(w, http.StatusUnauthorized, "Invalid authorization header")
return
}
accessToken := parts[1]
// Get user info
userInfo, err := oidcProvider.GetUserInfo(accessToken)
if err != nil {
writeError(w, http.StatusUnauthorized, err.Error())
return
}
writeJSON(w, http.StatusOK, userInfo)
}
// Helper Functions
// handleCORS handles CORS preflight requests
func handleCORS(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusOK)
}
// writeJSON writes JSON response
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// writeError writes error response
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
// generateID generates a simple ID
func generateID() string {
return time.Now().Format("20060102150405")
}
-405
View File
@@ -1,405 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// TestHealthEndpoint tests the health check endpoint
func TestHealthEndpoint(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
handleHealth(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var response map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if response["status"] != "healthy" {
t.Errorf("Expected status healthy, got %v", response["status"])
}
}
// TestPaymentInstruments tests getting payment instruments
func TestPaymentInstruments(t *testing.T) {
req := httptest.NewRequest("GET", "/payment/instruments", nil)
w := httptest.NewRecorder()
handlePaymentInstruments(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var instruments []map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&instruments); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if len(instruments) == 0 {
t.Error("Expected at least one payment instrument")
}
}
// TestCanMakePayment tests payment capability check
func TestCanMakePayment(t *testing.T) {
payload := map[string]interface{}{
"origin": "https://localhost:3000",
"methodData": []map[string]interface{}{
{
"supportedMethods": "https://motor.sonr.io/pay",
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/payment/canmakepayment", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handleCanMakePayment(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var response map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if response["canMakePayment"] != true {
t.Errorf("Expected canMakePayment to be true")
}
}
// TestProcessPayment tests payment processing with security
func TestProcessPayment(t *testing.T) {
// Initialize payment security
InitializePaymentSecurity()
payload := map[string]interface{}{
"origin": "https://localhost:3000",
"topOrigin": "https://localhost:3000",
"paymentRequestId": "test-request-123",
"methodData": []map[string]interface{}{
{
"supportedMethods": "https://motor.sonr.io/pay",
},
},
"details": map[string]interface{}{
"total": map[string]interface{}{
"label": "Test Payment",
"amount": map[string]interface{}{
"currency": "USD",
"value": "100.00",
},
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/payment/process", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://localhost:3000")
w := httptest.NewRecorder()
handleProcessPayment(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var response map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if response["paymentId"] == "" {
t.Error("Expected payment ID in response")
}
if response["status"] != "pending" {
t.Errorf("Expected status pending, got %v", response["status"])
}
}
// TestCardTokenization tests PCI-compliant card tokenization
func TestCardTokenization(t *testing.T) {
// Initialize payment security
InitializePaymentSecurity()
// Test valid card
token, err := TokenizeCard("4111111111111111", "123", 12, 2025)
if err != nil {
t.Fatalf("Failed to tokenize valid card: %v", err)
}
if token == "" {
t.Error("Expected token to be generated")
}
// Test invalid card number
_, err = TokenizeCard("1234567890123456", "123", 12, 2025)
if err == nil {
t.Error("Expected error for invalid card number")
}
// Test expired card
_, err = TokenizeCard("4111111111111111", "123", 1, 2020)
if err == nil {
t.Error("Expected error for expired card")
}
}
// TestTransactionSigning tests transaction signature verification
func TestTransactionSigning(t *testing.T) {
// Initialize payment security
InitializePaymentSecurity()
txData := map[string]interface{}{
"id": "test-tx-123",
"amount": "100.00",
"currency": "USD",
"method": "card",
"timestamp": time.Now().Unix(),
}
// Sign transaction
signature, err := SignTransaction(txData)
if err != nil {
t.Fatalf("Failed to sign transaction: %v", err)
}
if signature == "" {
t.Error("Expected signature to be generated")
}
// Verify signature
valid := VerifyTransactionSignature(txData, signature)
if !valid {
t.Error("Expected signature to be valid")
}
// Test invalid signature
invalid := VerifyTransactionSignature(txData, "invalid-signature")
if invalid {
t.Error("Expected invalid signature to fail verification")
}
}
// TestOIDCDiscovery tests OIDC discovery endpoint
func TestOIDCDiscovery(t *testing.T) {
req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
w := httptest.NewRecorder()
handleOIDCDiscovery(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var config map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&config); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
// Check required OIDC fields
requiredFields := []string{
"issuer",
"authorization_endpoint",
"token_endpoint",
"userinfo_endpoint",
"jwks_uri",
}
for _, field := range requiredFields {
if _, exists := config[field]; !exists {
t.Errorf("Missing required OIDC field: %s", field)
}
}
}
// TestJWKS tests JWKS endpoint
func TestJWKS(t *testing.T) {
req := httptest.NewRequest("GET", "/.well-known/jwks.json", nil)
w := httptest.NewRecorder()
handleJWKS(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var jwks map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&jwks); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
keys, ok := jwks["keys"].([]interface{})
if !ok || len(keys) == 0 {
t.Error("Expected at least one key in JWKS")
}
}
// TestRateLimiting tests rate limiting functionality
func TestRateLimiting(t *testing.T) {
// Initialize with low rate limit for testing
securityConfig.RateLimit = 5
securityConfig.RateWindow = time.Second
rateLimiter = NewRateLimiter(5, time.Second)
// Make requests up to the limit
for i := 0; i < 5; i++ {
req := httptest.NewRequest("GET", "/health", nil)
req.Header.Set("Origin", "test-client")
w := httptest.NewRecorder()
SecurityMiddleware(handleHealth)(w, req)
if w.Code != http.StatusOK {
t.Errorf("Request %d: Expected status 200, got %d", i+1, w.Code)
}
}
// Next request should be rate limited
req := httptest.NewRequest("GET", "/health", nil)
req.Header.Set("Origin", "test-client")
w := httptest.NewRecorder()
SecurityMiddleware(handleHealth)(w, req)
if w.Code != http.StatusTooManyRequests {
t.Errorf("Expected rate limit (429), got %d", w.Code)
}
// Wait for rate limit window to reset
time.Sleep(time.Second + 100*time.Millisecond)
// Should work again
req = httptest.NewRequest("GET", "/health", nil)
req.Header.Set("Origin", "test-client")
w = httptest.NewRecorder()
SecurityMiddleware(handleHealth)(w, req)
if w.Code != http.StatusOK {
t.Errorf("After reset: Expected status 200, got %d", w.Code)
}
}
// TestSecurityHeaders tests security headers are properly set
func TestSecurityHeaders(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
SecurityMiddleware(handleHealth)(w, req)
// Check security headers
headers := map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
}
for header, expected := range headers {
actual := w.Header().Get(header)
if actual != expected {
t.Errorf("Header %s: expected %s, got %s", header, expected, actual)
}
}
// Check CSP is present
csp := w.Header().Get("Content-Security-Policy")
if csp == "" {
t.Error("Expected Content-Security-Policy header")
}
}
// TestPCICompliance tests PCI compliance audit logging
func TestPCICompliance(t *testing.T) {
// Initialize payment security
InitializePaymentSecurity()
// Log some actions
pciCompliance.LogAction("TEST_ACTION", "user123", "resource456", "SUCCESS", "127.0.0.1")
// Get audit log
logs := pciCompliance.GetAuditLog(10)
if len(logs) == 0 {
t.Error("Expected audit log entries")
}
// Check last entry
lastLog := logs[len(logs)-1]
if lastLog.Action != "TEST_ACTION" {
t.Errorf("Expected action TEST_ACTION, got %s", lastLog.Action)
}
if lastLog.UserID != "user123" {
t.Errorf("Expected user ID user123, got %s", lastLog.UserID)
}
}
// TestDataEncryption tests sensitive data encryption
func TestDataEncryption(t *testing.T) {
// Initialize payment security
InitializePaymentSecurity()
sensitiveData := "4111-1111-1111-1111"
// Encrypt data
encrypted, err := EncryptSensitiveData(sensitiveData)
if err != nil {
t.Fatalf("Failed to encrypt data: %v", err)
}
if encrypted == sensitiveData {
t.Error("Encrypted data should not match plaintext")
}
// Decrypt data
decrypted, err := DecryptSensitiveData(encrypted)
if err != nil {
t.Fatalf("Failed to decrypt data: %v", err)
}
if decrypted != sensitiveData {
t.Errorf("Decrypted data doesn't match original: got %s, want %s", decrypted, sensitiveData)
}
}
// TestCardMasking tests card number masking
func TestCardMasking(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"4111111111111111", "**** **** **** 1111"},
{"5500000000000004", "**** **** **** 0004"},
{"340000000000009", "********** 00009"},
{"123", "123"}, // Too short to mask
}
for _, tc := range testCases {
masked := MaskCardNumber(tc.input)
if masked != tc.expected {
t.Errorf("MaskCardNumber(%s): got %s, want %s", tc.input, masked, tc.expected)
}
}
}
-273
View File
@@ -1,273 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
"time"
)
// JWTManager handles JWT token operations
type JWTManager struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
kid string
issuer string
}
// JWTHeader represents JWT header
type JWTHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
Kid string `json:"kid,omitempty"`
}
// JWTClaims represents standard JWT claims
type JWTClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience interface{} `json:"aud,omitempty"` // Can be string or []string
Expiration int64 `json:"exp,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
JWTID string `json:"jti,omitempty"`
Nonce string `json:"nonce,omitempty"`
Extra map[string]interface{} `json:"-"`
}
// IDToken represents an OpenID Connect ID token
type IDToken struct {
JWTClaims
AuthTime int64 `json:"auth_time,omitempty"`
Nonce string `json:"nonce,omitempty"`
ACR string `json:"acr,omitempty"`
AMR []string `json:"amr,omitempty"`
AZP string `json:"azp,omitempty"`
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
}
// Global JWT manager instance
var jwtManager *JWTManager
// InitJWTManager initializes the JWT manager
func InitJWTManager() error {
// Generate RSA key pair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("failed to generate RSA key: %w", err)
}
jwtManager = &JWTManager{
privateKey: privateKey,
publicKey: &privateKey.PublicKey,
kid: "motor-key-1",
issuer: "https://motor.sonr.io",
}
return nil
}
// GenerateToken generates a JWT token
func (m *JWTManager) GenerateToken(claims JWTClaims) (string, error) {
// Set standard claims
if claims.Issuer == "" {
claims.Issuer = m.issuer
}
if claims.IssuedAt == 0 {
claims.IssuedAt = time.Now().Unix()
}
if claims.Expiration == 0 {
claims.Expiration = time.Now().Add(1 * time.Hour).Unix()
}
// Create header
header := JWTHeader{
Alg: "RS256",
Typ: "JWT",
Kid: m.kid,
}
// Encode header
headerJSON, _ := json.Marshal(header)
headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON)
// Encode claims
claimsJSON, _ := json.Marshal(claims)
claimsEncoded := base64.RawURLEncoding.EncodeToString(claimsJSON)
// Create signature
message := headerEncoded + "." + claimsEncoded
hash := sha256.Sum256([]byte(message))
signature, err := rsa.SignPKCS1v15(rand.Reader, m.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
signatureEncoded := base64.RawURLEncoding.EncodeToString(signature)
// Combine parts
token := message + "." + signatureEncoded
return token, nil
}
// GenerateIDToken generates an OpenID Connect ID token
func (m *JWTManager) GenerateIDToken(subject, audience, nonce string, extra map[string]interface{}) (string, error) {
idToken := IDToken{
JWTClaims: JWTClaims{
Issuer: m.issuer,
Subject: subject,
Audience: audience,
IssuedAt: time.Now().Unix(),
Expiration: time.Now().Add(1 * time.Hour).Unix(),
Nonce: nonce,
},
AuthTime: time.Now().Unix(),
Email: fmt.Sprintf("%s@motor.sonr.io", subject),
EmailVerified: true,
}
// Convert to claims
claims := JWTClaims{
Issuer: idToken.Issuer,
Subject: idToken.Subject,
Audience: idToken.Audience,
IssuedAt: idToken.IssuedAt,
Expiration: idToken.Expiration,
Nonce: idToken.Nonce,
Extra: map[string]interface{}{
"auth_time": idToken.AuthTime,
"email": idToken.Email,
"email_verified": idToken.EmailVerified,
},
}
// Add extra claims
for k, v := range extra {
claims.Extra[k] = v
}
return m.GenerateToken(claims)
}
// ValidateToken validates a JWT token
func (m *JWTManager) ValidateToken(tokenString string) (*JWTClaims, error) {
// Split token
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// Decode header
headerJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return nil, fmt.Errorf("failed to decode header: %w", err)
}
var header JWTHeader
if err := json.Unmarshal(headerJSON, &header); err != nil {
return nil, fmt.Errorf("failed to parse header: %w", err)
}
// Verify algorithm
if header.Alg != "RS256" {
return nil, fmt.Errorf("unsupported algorithm: %s", header.Alg)
}
// Decode claims
claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to decode claims: %w", err)
}
var claims JWTClaims
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
return nil, fmt.Errorf("failed to parse claims: %w", err)
}
// Verify signature
message := parts[0] + "." + parts[1]
signature, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return nil, fmt.Errorf("failed to decode signature: %w", err)
}
hash := sha256.Sum256([]byte(message))
if err := rsa.VerifyPKCS1v15(m.publicKey, crypto.SHA256, hash[:], signature); err != nil {
return nil, fmt.Errorf("invalid signature: %w", err)
}
// Verify expiration
if claims.Expiration > 0 && time.Now().Unix() > claims.Expiration {
return nil, fmt.Errorf("token expired")
}
// Verify not before
if claims.NotBefore > 0 && time.Now().Unix() < claims.NotBefore {
return nil, fmt.Errorf("token not yet valid")
}
return &claims, nil
}
// GetPublicKeyJWK returns the public key in JWK format
func (m *JWTManager) GetPublicKeyJWK() map[string]interface{} {
// Get modulus and exponent
n := base64.RawURLEncoding.EncodeToString(m.publicKey.N.Bytes())
e := base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}) // 65537
return map[string]interface{}{
"kty": "RSA",
"use": "sig",
"kid": m.kid,
"alg": "RS256",
"n": n,
"e": e,
}
}
// GetPublicKeyPEM returns the public key in PEM format
func (m *JWTManager) GetPublicKeyPEM() string {
pubKeyBytes, _ := x509.MarshalPKIXPublicKey(m.publicKey)
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyBytes,
})
return string(pubKeyPEM)
}
// GenerateAccessToken generates an access token
func (m *JWTManager) GenerateAccessToken(subject, scope string) (string, error) {
claims := JWTClaims{
Subject: subject,
Extra: map[string]interface{}{
"scope": scope,
"token_type": "Bearer",
},
}
return m.GenerateToken(claims)
}
// GenerateRefreshToken generates a refresh token
func (m *JWTManager) GenerateRefreshToken(subject string) (string, error) {
claims := JWTClaims{
Subject: subject,
Expiration: time.Now().Add(30 * 24 * time.Hour).Unix(), // 30 days
Extra: map[string]interface{}{
"token_type": "refresh",
},
}
return m.GenerateToken(claims)
}
-50
View File
@@ -1,50 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"log"
"net/http"
wasmhttp "github.com/go-sonr/wasm-http-server/v3"
)
func main() {
// Set up HTTP routes
setupRoutes()
// Start the WASM HTTP server
log.Println("Motor Payment Gateway & OIDC Server starting...")
log.Println("Available endpoints:")
log.Println(" Health: /health, /status")
log.Println(" Payment API: /api/payment/*")
log.Println(" OIDC: /.well-known/*, /authorize, /token, /userinfo")
wasmhttp.Serve(nil)
}
// setupRoutes configures all HTTP routes with security middleware
func setupRoutes() {
// Health and status endpoints (no rate limiting)
http.HandleFunc("/health", handleHealth)
http.HandleFunc("/status", handleStatus)
// W3C Payment Handler API endpoints with security
http.HandleFunc("/payment/instruments", SecurityMiddleware(handlePaymentInstruments))
http.HandleFunc("/payment/canmakepayment", SecurityMiddleware(handleCanMakePayment))
http.HandleFunc("/payment/paymentrequest", SecurityMiddleware(handlePaymentRequest))
// Payment Gateway endpoints with security
http.HandleFunc("/api/payment/process", SecurityMiddleware(handlePaymentProcess))
http.HandleFunc("/api/payment/validate", SecurityMiddleware(handlePaymentValidate))
http.HandleFunc("/api/payment/status/", SecurityMiddleware(handlePaymentStatus))
http.HandleFunc("/api/payment/refund", SecurityMiddleware(handlePaymentRefund))
// OIDC endpoints with security
http.HandleFunc("/.well-known/openid-configuration", handleOIDCDiscovery) // No rate limit for discovery
http.HandleFunc("/.well-known/jwks.json", handleJWKS) // No rate limit for JWKS
http.HandleFunc("/authorize", SecurityMiddleware(handleAuthorize))
http.HandleFunc("/token", SecurityMiddleware(handleToken))
http.HandleFunc("/userinfo", SecurityMiddleware(handleUserInfo))
}
-364
View File
@@ -1,364 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
"sync"
"time"
)
// OIDCProvider manages OpenID Connect operations
type OIDCProvider struct {
mu sync.RWMutex
issuer string
authCodes map[string]*AuthorizationCode
accessTokens map[string]*AccessToken
refreshTokens map[string]*RefreshToken
clients map[string]*OIDCClient
users map[string]*User
}
// AuthorizationCode represents an authorization code
type AuthorizationCode struct {
Code string
ClientID string
RedirectURI string
Scope string
State string
Nonce string
UserID string
ExpiresAt time.Time
CodeChallenge string
CodeChallengeMethod string
}
// AccessToken represents an access token
type AccessToken struct {
Token string
ClientID string
UserID string
Scope string
ExpiresAt time.Time
}
// RefreshToken represents a refresh token
type RefreshToken struct {
Token string
ClientID string
UserID string
Scope string
ExpiresAt time.Time
}
// OIDCClient represents an OIDC client application
type OIDCClient struct {
ClientID string
ClientSecret string
RedirectURIs []string
GrantTypes []string
ResponseTypes []string
Scopes []string
Name string
}
// User represents a user
type User struct {
ID string
Username string
Email string
EmailVerified bool
Name string
GivenName string
FamilyName string
}
// OIDCDiscovery represents OIDC discovery document
type OIDCDiscovery struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
JWKSUri string `json:"jwks_uri"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
GrantTypesSupported []string `json:"grant_types_supported"`
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
// TokenRequest represents a token request
type TokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
CodeVerifier string `json:"code_verifier,omitempty"`
}
// TokenResponse represents a token response
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
Scope string `json:"scope,omitempty"`
}
// Global OIDC provider instance
var oidcProvider = &OIDCProvider{
issuer: "https://motor.sonr.io",
authCodes: make(map[string]*AuthorizationCode),
accessTokens: make(map[string]*AccessToken),
refreshTokens: make(map[string]*RefreshToken),
clients: make(map[string]*OIDCClient),
users: make(map[string]*User),
}
// Initialize OIDC provider
func init() {
// Initialize JWT manager
InitJWTManager()
// Add default client for testing
oidcProvider.clients["motor-client"] = &OIDCClient{
ClientID: "motor-client",
ClientSecret: "motor-secret",
RedirectURIs: []string{"https://localhost:3000/callback", "http://localhost:3000/callback"},
GrantTypes: []string{"authorization_code", "refresh_token"},
ResponseTypes: []string{"code", "token", "id_token"},
Scopes: []string{"openid", "profile", "email"},
Name: "Motor Test Client",
}
// Add default user for testing
oidcProvider.users["test-user"] = &User{
ID: "test-user",
Username: "testuser",
Email: "test@motor.sonr.io",
EmailVerified: true,
Name: "Test User",
GivenName: "Test",
FamilyName: "User",
}
}
// GetDiscovery returns OIDC discovery document
func (p *OIDCProvider) GetDiscovery() *OIDCDiscovery {
return &OIDCDiscovery{
Issuer: p.issuer,
AuthorizationEndpoint: "/authorize",
TokenEndpoint: "/token",
UserInfoEndpoint: "/userinfo",
JWKSUri: "/.well-known/jwks.json",
ScopesSupported: []string{
"openid", "profile", "email", "offline_access",
},
ResponseTypesSupported: []string{
"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token",
},
GrantTypesSupported: []string{
"authorization_code", "implicit", "refresh_token",
},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_basic", "client_secret_post",
},
ClaimsSupported: []string{
"sub", "name", "given_name", "family_name", "email", "email_verified",
},
CodeChallengeMethodsSupported: []string{"plain", "S256"},
}
}
// GenerateAuthorizationCode generates an authorization code
func (p *OIDCProvider) GenerateAuthorizationCode(clientID, redirectURI, scope, state, nonce, userID string, codeChallenge, codeChallengeMethod string) (*AuthorizationCode, error) {
p.mu.Lock()
defer p.mu.Unlock()
// Validate client
client, exists := p.clients[clientID]
if !exists {
return nil, fmt.Errorf("invalid client_id")
}
// Validate redirect URI
validRedirect := false
for _, uri := range client.RedirectURIs {
if uri == redirectURI {
validRedirect = true
break
}
}
if !validRedirect {
return nil, fmt.Errorf("invalid redirect_uri")
}
// Generate code
code := generateRandomString(32)
authCode := &AuthorizationCode{
Code: code,
ClientID: clientID,
RedirectURI: redirectURI,
Scope: scope,
State: state,
Nonce: nonce,
UserID: userID,
ExpiresAt: time.Now().Add(10 * time.Minute),
CodeChallenge: codeChallenge,
CodeChallengeMethod: codeChallengeMethod,
}
p.authCodes[code] = authCode
return authCode, nil
}
// ExchangeCode exchanges authorization code for tokens
func (p *OIDCProvider) ExchangeCode(req *TokenRequest) (*TokenResponse, error) {
p.mu.Lock()
defer p.mu.Unlock()
// Get authorization code
authCode, exists := p.authCodes[req.Code]
if !exists {
return nil, fmt.Errorf("invalid authorization code")
}
// Validate code hasn't expired
if time.Now().After(authCode.ExpiresAt) {
delete(p.authCodes, req.Code)
return nil, fmt.Errorf("authorization code expired")
}
// Validate client
if authCode.ClientID != req.ClientID {
return nil, fmt.Errorf("client_id mismatch")
}
// Validate redirect URI
if authCode.RedirectURI != req.RedirectURI {
return nil, fmt.Errorf("redirect_uri mismatch")
}
// Validate PKCE if present
if authCode.CodeChallenge != "" {
if !validatePKCE(authCode.CodeChallenge, authCode.CodeChallengeMethod, req.CodeVerifier) {
return nil, fmt.Errorf("invalid code_verifier")
}
}
// Delete used code
delete(p.authCodes, req.Code)
// Generate tokens
accessToken, _ := jwtManager.GenerateAccessToken(authCode.UserID, authCode.Scope)
refreshToken, _ := jwtManager.GenerateRefreshToken(authCode.UserID)
idToken, _ := jwtManager.GenerateIDToken(authCode.UserID, authCode.ClientID, authCode.Nonce, nil)
// Store tokens
p.accessTokens[accessToken] = &AccessToken{
Token: accessToken,
ClientID: authCode.ClientID,
UserID: authCode.UserID,
Scope: authCode.Scope,
ExpiresAt: time.Now().Add(1 * time.Hour),
}
p.refreshTokens[refreshToken] = &RefreshToken{
Token: refreshToken,
ClientID: authCode.ClientID,
UserID: authCode.UserID,
Scope: authCode.Scope,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
}
return &TokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: refreshToken,
IDToken: idToken,
Scope: authCode.Scope,
}, nil
}
// GetUserInfo returns user information
func (p *OIDCProvider) GetUserInfo(accessToken string) (map[string]interface{}, error) {
p.mu.RLock()
defer p.mu.RUnlock()
// Validate access token
token, exists := p.accessTokens[accessToken]
if !exists {
return nil, fmt.Errorf("invalid access token")
}
// Check expiration
if time.Now().After(token.ExpiresAt) {
return nil, fmt.Errorf("access token expired")
}
// Get user
user, exists := p.users[token.UserID]
if !exists {
return nil, fmt.Errorf("user not found")
}
// Return user info based on scope
userInfo := map[string]interface{}{
"sub": user.ID,
}
// Add claims based on scope
scopes := strings.Split(token.Scope, " ")
for _, scope := range scopes {
switch scope {
case "profile":
userInfo["name"] = user.Name
userInfo["given_name"] = user.GivenName
userInfo["family_name"] = user.FamilyName
userInfo["preferred_username"] = user.Username
case "email":
userInfo["email"] = user.Email
userInfo["email_verified"] = user.EmailVerified
}
}
return userInfo, nil
}
// Helper functions
// generateRandomString generates a random string
func generateRandomString(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return base64.RawURLEncoding.EncodeToString(bytes)[:length]
}
// validatePKCE validates PKCE code challenge
func validatePKCE(codeChallenge, method, verifier string) bool {
if method == "plain" {
return codeChallenge == verifier
}
// For S256, would need to implement SHA256 hashing
// For simplicity, returning true for now
return true
}
-354
View File
@@ -1,354 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"sync"
"time"
)
// PaymentMethod represents a payment method according to W3C Payment Handler API
type PaymentMethod struct {
SupportedMethods string `json:"supportedMethods"`
Data interface{} `json:"data,omitempty"`
}
// PaymentDetails contains payment details
type PaymentDetails struct {
Total PaymentItem `json:"total"`
DisplayItems []PaymentItem `json:"displayItems,omitempty"`
Modifiers []interface{} `json:"modifiers,omitempty"`
ShippingOptions []interface{} `json:"shippingOptions,omitempty"`
}
// PaymentItem represents an item in payment
type PaymentItem struct {
Label string `json:"label"`
Amount PaymentCurrency `json:"amount"`
}
// PaymentCurrency represents currency amount
type PaymentCurrency struct {
Currency string `json:"currency"`
Value string `json:"value"`
}
// PaymentRequest represents a W3C Payment Request
type PaymentRequest struct {
ID string `json:"id"`
MethodData []PaymentMethod `json:"methodData"`
Details PaymentDetails `json:"details"`
Options PaymentOptions `json:"options,omitempty"`
Origin string `json:"origin"`
TopOrigin string `json:"topOrigin"`
PaymentRequestID string `json:"paymentRequestId"`
Total PaymentItem `json:"total"`
}
// PaymentOptions contains payment options
type PaymentOptions struct {
RequestPayerName bool `json:"requestPayerName,omitempty"`
RequestPayerEmail bool `json:"requestPayerEmail,omitempty"`
RequestPayerPhone bool `json:"requestPayerPhone,omitempty"`
RequestShipping bool `json:"requestShipping,omitempty"`
ShippingType string `json:"shippingType,omitempty"`
}
// PaymentResponse represents response to payment request
type PaymentResponse struct {
RequestID string `json:"requestId"`
MethodName string `json:"methodName"`
Details map[string]interface{} `json:"details"`
PayerName string `json:"payerName,omitempty"`
PayerEmail string `json:"payerEmail,omitempty"`
PayerPhone string `json:"payerPhone,omitempty"`
ShippingAddress interface{} `json:"shippingAddress,omitempty"`
}
// PaymentTransaction represents a payment transaction
type PaymentTransaction struct {
ID string `json:"id"`
Status string `json:"status"`
Amount PaymentCurrency `json:"amount"`
Method string `json:"method"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Request *PaymentRequest `json:"request,omitempty"`
Response *PaymentResponse `json:"response,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// PaymentHandler manages payment processing
type PaymentHandler struct {
mu sync.RWMutex
transactions map[string]*PaymentTransaction
instruments []PaymentInstrument
}
// PaymentInstrument represents a payment instrument
type PaymentInstrument struct {
Name string `json:"name"`
Icons []Icon `json:"icons,omitempty"`
Method string `json:"method"`
Capabilities []string `json:"capabilities,omitempty"`
}
// Icon represents a payment instrument icon
type Icon struct {
Src string `json:"src"`
Sizes string `json:"sizes,omitempty"`
Type string `json:"type,omitempty"`
}
// Global payment handler instance
var paymentHandler = &PaymentHandler{
transactions: make(map[string]*PaymentTransaction),
instruments: []PaymentInstrument{
{
Name: "Motor Payment",
Method: "https://motor.sonr.io/pay",
Capabilities: []string{"basic-card", "tokenized-card"},
},
},
}
// ProcessPayment processes a payment request with enhanced security
func (h *PaymentHandler) ProcessPayment(req *PaymentRequest) (*PaymentTransaction, error) {
h.mu.Lock()
defer h.mu.Unlock()
// Initialize payment security if not already done
InitializePaymentSecurity()
// Validate origin for security
if !ValidateOrigin(req.Origin) {
return nil, fmt.Errorf("invalid origin: %s", req.Origin)
}
// Generate transaction ID
txID := generateTransactionID()
// Create transaction data for signing
txData := map[string]interface{}{
"id": txID,
"amount": req.Details.Total.Amount.Value,
"currency": req.Details.Total.Amount.Currency,
"method": req.MethodData[0].SupportedMethods,
"timestamp": time.Now().Unix(),
}
// Sign transaction for integrity
signature, err := SignTransaction(txData)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %v", err)
}
// Create transaction
tx := &PaymentTransaction{
ID: txID,
Status: "pending",
Amount: req.Details.Total.Amount,
Method: req.MethodData[0].SupportedMethods,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Request: req,
Metadata: map[string]interface{}{
"origin": req.Origin,
"topOrigin": req.TopOrigin,
"signature": signature,
},
}
// Log for PCI compliance
pciCompliance.LogAction("PROCESS_PAYMENT", "", txID, "INITIATED", req.Origin)
// Store transaction
h.transactions[txID] = tx
// Process payment asynchronously with security checks
go h.processPaymentSecurely(txID)
return tx, nil
}
// ValidatePaymentMethod validates a payment method
func (h *PaymentHandler) ValidatePaymentMethod(method string, data interface{}) (bool, error) {
// Check if method is supported
for _, instrument := range h.instruments {
if instrument.Method == method {
// Perform validation based on method type
switch method {
case "basic-card", "https://motor.sonr.io/pay":
return h.validateCardData(data)
default:
return true, nil
}
}
}
return false, nil
}
// GetTransaction retrieves a transaction by ID
func (h *PaymentHandler) GetTransaction(id string) (*PaymentTransaction, bool) {
h.mu.RLock()
defer h.mu.RUnlock()
tx, exists := h.transactions[id]
return tx, exists
}
// UpdateTransactionStatus updates transaction status
func (h *PaymentHandler) UpdateTransactionStatus(id, status string) error {
h.mu.Lock()
defer h.mu.Unlock()
if tx, exists := h.transactions[id]; exists {
tx.Status = status
tx.UpdatedAt = time.Now()
return nil
}
return nil
}
// CanMakePayment checks if payment can be made
func (h *PaymentHandler) CanMakePayment(methods []PaymentMethod) bool {
for _, method := range methods {
for _, instrument := range h.instruments {
if instrument.Method == method.SupportedMethods {
return true
}
}
}
return false
}
// GetInstruments returns available payment instruments
func (h *PaymentHandler) GetInstruments() []PaymentInstrument {
return h.instruments
}
// Helper functions
// generateTransactionID generates a unique transaction ID
func generateTransactionID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return "txn_" + hex.EncodeToString(bytes)
}
// validateCardData validates and tokenizes card payment data
func (h *PaymentHandler) validateCardData(data interface{}) (bool, error) {
// Initialize payment security
InitializePaymentSecurity()
if data == nil {
return false, fmt.Errorf("no payment data provided")
}
// Parse card data
cardData, ok := data.(map[string]interface{})
if !ok {
return false, fmt.Errorf("invalid payment data format")
}
// Extract card details
cardNumber, hasNumber := cardData["cardNumber"].(string)
cvv, hasCVV := cardData["cvv"].(string)
expiryMonth, hasMonth := cardData["expiryMonth"].(float64)
expiryYear, hasYear := cardData["expiryYear"].(float64)
if !hasNumber || !hasCVV || !hasMonth || !hasYear {
return false, fmt.Errorf("missing required card fields")
}
// Tokenize the card for PCI compliance
token, err := TokenizeCard(cardNumber, cvv, int(expiryMonth), int(expiryYear))
if err != nil {
return false, fmt.Errorf("card validation failed: %v", err)
}
// Replace sensitive data with token
cardData["token"] = token
cardData["cardNumber"] = MaskCardNumber(cardNumber)
delete(cardData, "cvv") // Never store CVV
return true, nil
}
// processPaymentSecurely processes payment with enhanced security
func (h *PaymentHandler) processPaymentSecurely(txID string) {
// Initialize payment security
InitializePaymentSecurity()
// Simulate processing delay
time.Sleep(2 * time.Second)
// Verify transaction exists
h.mu.RLock()
tx, exists := h.transactions[txID]
h.mu.RUnlock()
if !exists {
pciCompliance.LogAction("PROCESS_PAYMENT", "", txID, "FAILED", "Transaction not found")
return
}
// Verify transaction signature
txData := map[string]interface{}{
"id": txID,
"amount": tx.Amount.Value,
"currency": tx.Amount.Currency,
"method": tx.Method,
"timestamp": tx.CreatedAt.Unix(),
}
if signature, ok := tx.Metadata["signature"].(string); ok {
if !VerifyTransactionSignature(txData, signature) {
h.UpdateTransactionStatus(txID, "failed")
pciCompliance.LogAction("PROCESS_PAYMENT", "", txID, "FAILED", "Invalid signature")
return
}
}
// Update status to completed
h.UpdateTransactionStatus(txID, "completed")
// Create secure payment response
h.mu.Lock()
if tx, exists := h.transactions[txID]; exists {
// Generate response token
responseToken := generateSecureToken()
tx.Response = &PaymentResponse{
RequestID: tx.Request.PaymentRequestID,
MethodName: tx.Method,
Details: map[string]interface{}{
"transactionId": txID,
"status": "success",
"token": responseToken,
"timestamp": time.Now().Unix(),
},
}
// Log successful payment
pciCompliance.LogAction("PROCESS_PAYMENT", "", txID, "SUCCESS", tx.Request.Origin)
}
h.mu.Unlock()
}
// SerializePaymentRequest serializes a payment request from JSON
func SerializePaymentRequest(data []byte) (*PaymentRequest, error) {
var req PaymentRequest
err := json.Unmarshal(data, &req)
return &req, err
}
// SerializePaymentResponse serializes a payment response to JSON
func SerializePaymentResponse(resp *PaymentResponse) ([]byte, error) {
return json.Marshal(resp)
}
-454
View File
@@ -1,454 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"regexp"
"strings"
"sync"
"time"
)
// PaymentTokenizer handles secure payment method tokenization
type PaymentTokenizer struct {
mu sync.RWMutex
tokens map[string]*TokenData
encryptKey []byte
}
// TokenData stores tokenized payment data
type TokenData struct {
Token string `json:"token"`
LastFour string `json:"last_four"`
Brand string `json:"brand"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CreatedAt time.Time `json:"created_at"`
UsedCount int `json:"used_count"`
}
// TransactionSigner handles transaction signing and verification
type TransactionSigner struct {
signKey []byte
}
// PCICompliance handles PCI DSS compliance requirements
type PCICompliance struct {
auditLog []AuditEntry
mu sync.RWMutex
}
// AuditEntry for PCI compliance logging
type AuditEntry struct {
Timestamp time.Time `json:"timestamp"`
Action string `json:"action"`
UserID string `json:"user_id"`
ResourceID string `json:"resource_id"`
Result string `json:"result"`
IPAddress string `json:"ip_address"`
}
var (
paymentTokenizer *PaymentTokenizer
transactionSigner *TransactionSigner
pciCompliance *PCICompliance
initOnce sync.Once
)
// InitializePaymentSecurity initializes payment security components
func InitializePaymentSecurity() {
initOnce.Do(func() {
// Generate encryption key (in production, use KMS)
encKey := make([]byte, 32)
io.ReadFull(rand.Reader, encKey)
// Generate signing key
signKey := make([]byte, 32)
io.ReadFull(rand.Reader, signKey)
paymentTokenizer = &PaymentTokenizer{
tokens: make(map[string]*TokenData),
encryptKey: encKey,
}
transactionSigner = &TransactionSigner{
signKey: signKey,
}
pciCompliance = &PCICompliance{
auditLog: make([]AuditEntry, 0),
}
})
}
// TokenizeCard tokenizes credit card data (PCI DSS compliant)
func TokenizeCard(cardNumber, cvv string, expiryMonth, expiryYear int) (string, error) {
// Validate card number using Luhn algorithm
if !validateLuhn(cardNumber) {
return "", fmt.Errorf("invalid card number")
}
// Validate CVV
if !validateCVV(cvv) {
return "", fmt.Errorf("invalid CVV")
}
// Validate expiry
if !validateExpiry(expiryMonth, expiryYear) {
return "", fmt.Errorf("card expired or invalid expiry date")
}
// Extract card info
lastFour := cardNumber[len(cardNumber)-4:]
brand := detectCardBrand(cardNumber)
// Generate secure token
token := generateSecureToken()
// Store tokenized data (never store raw card data)
tokenData := &TokenData{
Token: token,
LastFour: lastFour,
Brand: brand,
ExpiryMonth: expiryMonth,
ExpiryYear: expiryYear,
CreatedAt: time.Now(),
UsedCount: 0,
}
paymentTokenizer.mu.Lock()
paymentTokenizer.tokens[token] = tokenData
paymentTokenizer.mu.Unlock()
// Log tokenization for PCI compliance
pciCompliance.LogAction("TOKENIZE_CARD", "", token, "SUCCESS", "")
return token, nil
}
// validateLuhn validates credit card number using Luhn algorithm
func validateLuhn(cardNumber string) bool {
// Remove spaces and dashes
cardNumber = strings.ReplaceAll(cardNumber, " ", "")
cardNumber = strings.ReplaceAll(cardNumber, "-", "")
// Check if all digits
if !regexp.MustCompile(`^\d+$`).MatchString(cardNumber) {
return false
}
// Luhn algorithm
sum := 0
isEven := false
for i := len(cardNumber) - 1; i >= 0; i-- {
digit := int(cardNumber[i] - '0')
if isEven {
digit *= 2
if digit > 9 {
digit -= 9
}
}
sum += digit
isEven = !isEven
}
return sum%10 == 0
}
// validateCVV validates CVV format
func validateCVV(cvv string) bool {
// CVV should be 3 or 4 digits
return regexp.MustCompile(`^\d{3,4}$`).MatchString(cvv)
}
// validateExpiry validates card expiry date
func validateExpiry(month, year int) bool {
now := time.Now()
currentYear := now.Year()
currentMonth := int(now.Month())
// Check valid month
if month < 1 || month > 12 {
return false
}
// Check if expired
if year < currentYear || (year == currentYear && month < currentMonth) {
return false
}
// Check reasonable future date (max 20 years)
if year > currentYear+20 {
return false
}
return true
}
// detectCardBrand detects card brand from number
func detectCardBrand(cardNumber string) string {
// Remove spaces and dashes
cardNumber = strings.ReplaceAll(cardNumber, " ", "")
cardNumber = strings.ReplaceAll(cardNumber, "-", "")
// Visa
if strings.HasPrefix(cardNumber, "4") {
return "visa"
}
// Mastercard
if regexp.MustCompile(`^5[1-5]`).MatchString(cardNumber) ||
regexp.MustCompile(`^2[2-7]`).MatchString(cardNumber) {
return "mastercard"
}
// American Express
if strings.HasPrefix(cardNumber, "34") || strings.HasPrefix(cardNumber, "37") {
return "amex"
}
// Discover
if strings.HasPrefix(cardNumber, "6011") || strings.HasPrefix(cardNumber, "65") {
return "discover"
}
return "unknown"
}
// generateSecureToken generates a cryptographically secure token
func generateSecureToken() string {
b := make([]byte, 32)
rand.Read(b)
return "tok_" + base64.URLEncoding.EncodeToString(b)
}
// SignTransaction signs a transaction for integrity
func SignTransaction(transactionData map[string]interface{}) (string, error) {
// Serialize transaction data
data, err := json.Marshal(transactionData)
if err != nil {
return "", err
}
// Create HMAC signature
h := hmac.New(sha256.New, transactionSigner.signKey)
h.Write(data)
signature := hex.EncodeToString(h.Sum(nil))
// Log signing for audit
txID := ""
if id, ok := transactionData["id"].(string); ok {
txID = id
}
pciCompliance.LogAction("SIGN_TRANSACTION", "", txID, "SUCCESS", "")
return signature, nil
}
// VerifyTransactionSignature verifies a transaction signature
func VerifyTransactionSignature(transactionData map[string]interface{}, signature string) bool {
// Serialize transaction data
data, err := json.Marshal(transactionData)
if err != nil {
return false
}
// Create HMAC signature
h := hmac.New(sha256.New, transactionSigner.signKey)
h.Write(data)
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Compare signatures
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
// EncryptSensitiveData encrypts sensitive payment data
func EncryptSensitiveData(plaintext string) (string, error) {
// Create cipher
block, err := aes.NewCipher(paymentTokenizer.encryptKey)
if err != nil {
return "", err
}
// Generate nonce
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// Encrypt
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
ciphertext := aesgcm.Seal(nil, nonce, []byte(plaintext), nil)
// Combine nonce and ciphertext
combined := append(nonce, ciphertext...)
return base64.StdEncoding.EncodeToString(combined), nil
}
// DecryptSensitiveData decrypts sensitive payment data
func DecryptSensitiveData(encrypted string) (string, error) {
// Decode from base64
combined, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return "", err
}
// Extract nonce and ciphertext
if len(combined) < 12 {
return "", fmt.Errorf("invalid encrypted data")
}
nonce := combined[:12]
ciphertext := combined[12:]
// Create cipher
block, err := aes.NewCipher(paymentTokenizer.encryptKey)
if err != nil {
return "", err
}
// Decrypt
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// LogAction logs an action for PCI compliance audit
func (p *PCICompliance) LogAction(action, userID, resourceID, result, ipAddress string) {
p.mu.Lock()
defer p.mu.Unlock()
entry := AuditEntry{
Timestamp: time.Now(),
Action: action,
UserID: userID,
ResourceID: resourceID,
Result: result,
IPAddress: ipAddress,
}
p.auditLog = append(p.auditLog, entry)
// In production, persist to secure audit log storage
// For now, just keep in memory (limited to last 10000 entries)
if len(p.auditLog) > 10000 {
p.auditLog = p.auditLog[1:]
}
}
// GetAuditLog returns recent audit log entries
func (p *PCICompliance) GetAuditLog(limit int) []AuditEntry {
p.mu.RLock()
defer p.mu.RUnlock()
if limit > len(p.auditLog) {
limit = len(p.auditLog)
}
// Return most recent entries
start := len(p.auditLog) - limit
if start < 0 {
start = 0
}
return p.auditLog[start:]
}
// ValidateToken validates a payment token
func ValidateToken(token string) (*TokenData, error) {
paymentTokenizer.mu.RLock()
defer paymentTokenizer.mu.RUnlock()
tokenData, exists := paymentTokenizer.tokens[token]
if !exists {
return nil, fmt.Errorf("invalid token")
}
// Check if token is expired (tokens valid for 1 hour)
if time.Since(tokenData.CreatedAt) > time.Hour {
return nil, fmt.Errorf("token expired")
}
// Increment usage count
tokenData.UsedCount++
return tokenData, nil
}
// MaskCardNumber masks all but last 4 digits of card number
func MaskCardNumber(cardNumber string) string {
// Remove spaces and dashes
cardNumber = strings.ReplaceAll(cardNumber, " ", "")
cardNumber = strings.ReplaceAll(cardNumber, "-", "")
if len(cardNumber) < 4 {
return strings.Repeat("*", len(cardNumber))
}
lastFour := cardNumber[len(cardNumber)-4:]
masked := strings.Repeat("*", len(cardNumber)-4) + lastFour
// Format based on card type
if len(masked) == 16 {
// Format as XXXX XXXX XXXX 1234
return masked[:4] + " " + masked[4:8] + " " + masked[8:12] + " " + masked[12:]
}
return masked
}
// SanitizePaymentData removes sensitive data from payment objects
func SanitizePaymentData(data map[string]interface{}) map[string]interface{} {
sanitized := make(map[string]interface{})
// List of sensitive fields to exclude
sensitiveFields := []string{
"card_number", "cvv", "cvc", "card_code",
"account_number", "routing_number", "pin",
}
for key, value := range data {
// Check if field is sensitive
isSensitive := false
keyLower := strings.ToLower(key)
for _, sensitive := range sensitiveFields {
if strings.Contains(keyLower, sensitive) {
isSensitive = true
break
}
}
if !isSensitive {
sanitized[key] = value
}
}
return sanitized
}
-242
View File
@@ -1,242 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
)
// RateLimiter implements rate limiting
type RateLimiter struct {
mu sync.RWMutex
requests map[string]*RequestCounter
limit int
window time.Duration
}
// RequestCounter tracks requests
type RequestCounter struct {
Count int
ResetTime time.Time
}
// SecurityConfig holds security configuration
type SecurityConfig struct {
EnableRateLimit bool
RateLimit int
RateWindow time.Duration
EnableCSP bool
CSPPolicy string
}
// Global security configuration
var securityConfig = &SecurityConfig{
EnableRateLimit: true,
RateLimit: 100, // 100 requests
RateWindow: time.Minute,
EnableCSP: true,
CSPPolicy: "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https:; img-src 'self' data: https:; style-src 'self' 'unsafe-inline';",
}
// Global rate limiter
var rateLimiter = NewRateLimiter(securityConfig.RateLimit, securityConfig.RateWindow)
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string]*RequestCounter),
limit: limit,
window: window,
}
}
// Allow checks if request is allowed
func (rl *RateLimiter) Allow(identifier string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
counter, exists := rl.requests[identifier]
if !exists || now.After(counter.ResetTime) {
// Create new counter or reset existing one
rl.requests[identifier] = &RequestCounter{
Count: 1,
ResetTime: now.Add(rl.window),
}
return true
}
if counter.Count >= rl.limit {
return false
}
counter.Count++
return true
}
// SecurityMiddleware wraps handlers with security features
func SecurityMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Apply security headers
applySecurityHeaders(w)
// Rate limiting
if securityConfig.EnableRateLimit {
// Use client IP or a default identifier for WASM environment
identifier := getClientIdentifier(r)
if !rateLimiter.Allow(identifier) {
writeError(w, http.StatusTooManyRequests, "Rate limit exceeded")
return
}
}
// Call the next handler
next(w, r)
}
}
// applySecurityHeaders applies security headers to response
func applySecurityHeaders(w http.ResponseWriter) {
// CORS headers (already handled by handleCORS, but adding for completeness)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// Security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
// Content Security Policy
if securityConfig.EnableCSP {
w.Header().Set("Content-Security-Policy", securityConfig.CSPPolicy)
}
// Strict Transport Security (for HTTPS)
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
}
// getClientIdentifier gets a client identifier for rate limiting
func getClientIdentifier(r *http.Request) string {
// In WASM environment, we can't rely on real IP
// Use a combination of headers for identification
// Try X-Forwarded-For
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return xff
}
// Try X-Real-IP
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
// Try Origin header (common in browser requests)
if origin := r.Header.Get("Origin"); origin != "" {
return origin
}
// Try User-Agent as last resort
if ua := r.Header.Get("User-Agent"); ua != "" {
return ua
}
// Default identifier
return "default-client"
}
// ValidatePaymentData validates payment data for security
func ValidatePaymentData(data map[string]interface{}) error {
// Check for required fields
requiredFields := []string{"amount", "currency"}
for _, field := range requiredFields {
if _, exists := data[field]; !exists {
return fmt.Errorf("missing required field: %s", field)
}
}
// Validate amount
if amount, ok := data["amount"].(float64); ok {
if amount <= 0 || amount > 1000000 {
return fmt.Errorf("invalid amount")
}
}
// Validate currency
if currency, ok := data["currency"].(string); ok {
validCurrencies := []string{"USD", "EUR", "GBP", "JPY"}
valid := false
for _, vc := range validCurrencies {
if currency == vc {
valid = true
break
}
}
if !valid {
return fmt.Errorf("unsupported currency")
}
}
return nil
}
// SanitizeInput sanitizes user input
func SanitizeInput(input string) string {
// Remove any potentially dangerous characters
// This is a basic implementation - in production, use a proper sanitization library
sanitized := input
// Remove script tags
sanitized = strings.ReplaceAll(sanitized, "<script>", "")
sanitized = strings.ReplaceAll(sanitized, "</script>", "")
// Remove other potentially dangerous HTML
sanitized = strings.ReplaceAll(sanitized, "<iframe>", "")
sanitized = strings.ReplaceAll(sanitized, "</iframe>", "")
// Limit length
if len(sanitized) > 1000 {
sanitized = sanitized[:1000]
}
return sanitized
}
// TokenizePaymentMethod creates a token for payment method
func TokenizePaymentMethod(method map[string]interface{}) string {
// Create a secure token representing the payment method
// In production, this would use proper tokenization service
token := generateRandomString(32)
// Store token mapping (in production, use secure storage)
// For now, just return the token
return "pmtoken_" + token
}
// ValidateOrigin validates request origin
func ValidateOrigin(origin string) bool {
// List of allowed origins
allowedOrigins := []string{
"https://motor.sonr.io",
"https://localhost:3000",
"http://localhost:3000",
"https://sonr.io",
}
for _, allowed := range allowedOrigins {
if origin == allowed {
return true
}
}
return false
}
-4
View File
@@ -1,4 +0,0 @@
package main
// Version is set by commitizen during release process
var Version = "dev"
-331
View File
@@ -1,331 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
---
version: 2
dist: dist/snrd
monorepo:
tag_prefix: snrd/
project_name: snrd
before:
hooks:
- go mod download
builds:
# Darwin AMD64 Build
- id: snrd-darwin-amd64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
- CGO_LDFLAGS=-lm
goos:
- darwin
goarch:
- amd64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Darwin ARM64 Build
- id: snrd-darwin-arm64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=oa64-clang
- CXX=oa64-clang++
- CGO_LDFLAGS=-lm
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Linux AMD64 Build
- id: snrd-linux-amd64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++
- CGO_LDFLAGS=-lm
goos:
- linux
goarch:
- amd64
goamd64:
- v1
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Linux ARM64 Build
- id: snrd-linux-arm64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
- CGO_LDFLAGS=-lm
goos:
- linux
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
aur_sources:
- name: snrd
disable: true
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity and data storage network"
maintainers:
- "Sonr <support@sonr.io>"
license: "GPL-3.0"
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://[email protected]/snrd.git"
skip_upload: auto
provides:
- snrd
conflicts:
- snrd-bin
depends:
- glibc
makedepends:
- go
- git
- make
commit_msg_template: "Update to {{ .Tag }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
prepare: |-
cd "${pkgname}_${pkgver}"
go mod download
build: |-
cd "${pkgname}_${pkgver}"
export CGO_ENABLED=1
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw"
go build \
-ldflags="-w -s -buildid='' -linkmode=external \
-X github.com/cosmos/cosmos-sdk/version.Name=sonr \
-X github.com/cosmos/cosmos-sdk/version.AppName=snrd \
-X github.com/cosmos/cosmos-sdk/version.Version=${pkgver} \
-X 'github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger'" \
-tags "netgo,ledger" \
-o snrd ./cmd/snrd
chmod +x ./snrd
package: |-
cd "${pkgname}_${pkgver}"
# bin
install -Dm755 "./snrd" "${pkgdir}/usr/bin/snrd"
# license
if [ -f "./LICENSE" ]; then
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/snrd/LICENSE"
fi
# readme
if [ -f "./README.md" ]; then
install -Dm644 "./README.md" "${pkgdir}/usr/share/doc/snrd/README.md"
fi
# config directory
install -dm755 "${pkgdir}/etc/snrd"
install -dm755 "${pkgdir}/var/lib/snrd"
backup:
- /etc/snrd/config.toml
- /etc/snrd/app.toml
nix:
- name: snrd
ids:
- snrd
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity network"
license: "gpl3"
path: pkgs/snrd/default.nix
commit_msg_template: "snrd: {{ .Tag }}"
dependencies:
- stdenv
- glibc
extra_install: |-
wrapProgram $out/bin/snrd --prefix PATH : ${lib.makeBinPath [ glibc stdenv.cc.cc.lib ]}
repository:
owner: sonr-io
name: nur
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
archives:
- id: snrd
ids:
- snrd-linux-amd64
- snrd-linux-arm64
- snrd-darwin-amd64
- snrd-darwin-arm64
name_template: >-
snrd_{{ .Os }}_{{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }}
formats: ["tar.gz"]
files:
- src: README*
wrap_in_directory: false
homebrew_casks:
- name: snrd
ids:
- snrd
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity and data storage network"
commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"
directory: Casks
repository:
owner: sonr-io
name: homebrew-tap
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/snrd"]
end
nfpms:
- id: snrd
package_name: snrd
ids:
- snrd-linux-amd64
- snrd-linux-arm64
file_name_template: "snrd_{{ .Os }}_{{ .Arch }}{{ .ConventionalExtension }}"
vendor: Sonr
homepage: "https://sonr.io"
maintainer: "Sonr <support@sonr.io>"
description: "Sonr is a decentralized, permissionless, and censorship-resistant identity network."
license: "GPL-3.0"
formats:
- rpm
- deb
- apk
- archlinux
contents:
- src: README*
dst: /usr/share/doc/snrd
bindir: /usr/bin
section: net
priority: optional
blobs:
- provider: s3
endpoint: https://eb37925850388bca807b7fab964c12bb.r2.cloudflarestorage.com
bucket: releases
region: auto
directory: "snrd/{{ .Tag }}"
ids:
- snrd
release:
disable: false
github:
owner: sonr-io
name: sonr
name_template: "{{.ProjectName}}/{{ .Tag }}"
draft: false
replace_existing_draft: false # Don't replace drafts
replace_existing_artifacts: false # Append, don't replace
mode: append # Explicitly set to append mode
checksum:
name_template: "snrd_checksums.txt"
npms:
- name: "@sonr.io/snrd"
ids:
- snrd
description: "Sonr blockchain daemon - decentralized identity and data storage network"
homepage: "https://sonr.io"
license: "GPL-3.0"
author: "Sonr <support@sonr.io>"
repository: "https://github.com/sonr-io/sonr"
bugs: "https://github.com/sonr-io/sonr/issues"
keywords:
- blockchain
- cosmos
- did
- identity
- web3
- sonr
access: public
format: tar.gz
snapshot:
version_template: "{{ incpatch .Version }}-dev"
# Changelog configuration
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
-5
View File
@@ -1,5 +0,0 @@
## snrd/v0.0.3 (2025-10-04)
## snrd/v0.0.2 (2025-10-04)
## snrd/v0.0.1 (2025-10-03)
-89
View File
@@ -1,89 +0,0 @@
# Use build argument for base image to allow using pre-cached base
ARG BASE_IMAGE=golang:1.24.7-alpine3.22
FROM ${BASE_IMAGE} AS builder
SHELL ["/bin/sh", "-ecuxo", "pipefail"]
# Install build dependencies
RUN apk add --no-cache \
ca-certificates \
build-base \
git \
linux-headers \
bash \
binutils-gold \
wget
WORKDIR /code
# Copy entire source code (including crypto module)
COPY . .
# Fix git ownership issue
RUN git config --global --add safe.directory /code
# Download WasmVM library
RUN --mount=type=cache,target=/tmp/wasmvm \
set -eux; \
export ARCH=$(uname -m); \
WASM_VERSION=$(go list -m all | grep github.com/CosmWasm/wasmvm | head -1 || echo ""); \
if [ ! -z "${WASM_VERSION}" ]; then \
WASMVM_REPO=$(echo $WASM_VERSION | awk '{print $1}'); \
WASMVM_VERS=$(echo $WASM_VERSION | awk '{print $2}'); \
WASMVM_FILE="libwasmvm_muslc.${ARCH}.a"; \
if [ ! -f "/tmp/wasmvm/${WASMVM_FILE}" ]; then \
wget -O "/tmp/wasmvm/${WASMVM_FILE}" "https://${WASMVM_REPO}/releases/download/${WASMVM_VERS}/${WASMVM_FILE}"; \
fi; \
cp "/tmp/wasmvm/${WASMVM_FILE}" /lib/libwasmvm_muslc.a; \
fi
# Download Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
# Build binary with optimizations
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev"); \
COMMIT=$(git log -1 --format='%H' 2>/dev/null || echo "unknown"); \
LEDGER_ENABLED=false BUILD_TAGS=muslc LINK_STATICALLY=true \
CGO_ENABLED=1 GOOS=linux \
go build \
-mod=readonly \
-tags "netgo,ledger,muslc" \
-ldflags "-X github.com/cosmos/cosmos-sdk/version.Name=sonr \
-X github.com/cosmos/cosmos-sdk/version.AppName=snrd \
-X github.com/cosmos/cosmos-sdk/version.Version=${VERSION} \
-X github.com/cosmos/cosmos-sdk/version.Commit=${COMMIT} \
-X github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger,muslc \
-w -s -linkmode=external -extldflags '-Wl,-z,muldefs -static'" \
-buildvcs=false \
-trimpath \
-o /code/build/snrd \
./cmd/snrd; \
file /code/build/snrd; \
echo "Ensuring binary is statically linked ..."; \
(file /code/build/snrd | grep "statically linked")
# --------------------------------------------------------
# Final minimal runtime image
FROM alpine:3.17
LABEL org.opencontainers.image.title="Sonr Daemon"
LABEL org.opencontainers.image.source="https://github.com/sonr-io/sonr"
# Copy binary from build stage
COPY --from=builder /code/build/snrd /usr/bin
COPY --from=builder /lib/libwasmvm_muslc.a /lib/libwasmvm_muslc.a
COPY --from=builder /code/scripts/test_node.sh /usr/bin/testnet
RUN chmod +x /usr/bin/testnet
# Set up dependencies
ENV PACKAGES="curl make bash jq sed"
# Install minimum necessary dependencies
RUN apk add --no-cache $PACKAGES
WORKDIR /opt
+2 -4
View File
@@ -109,13 +109,11 @@ test:
release:
@echo "Creating snrd release..."
@cd $(GIT_ROOT) && cz --config cmd/snrd/.cz.toml --no-raise 6,21 bump --yes --increment PATCH
@cd $(GIT_ROOT) && goreleaser release --clean
snapshot:
@echo "Dry-Run Bumping snrd version..."
@cd $(GIT_ROOT) && cz --config cmd/snrd/.cz.toml bump --yes --dry-run --no-verify --increment PATCH
@echo "Creating snrd snapshots for all platforms..."
@cd $(GIT_ROOT) && goreleaser release --snapshot --clean -f cmd/snrd/.goreleaser.yml
@cd $(GIT_ROOT) && goreleaser release --snapshot --clean
# Docker build targets
docker:
-441
View File
@@ -1,441 +0,0 @@
module snrd
go 1.24.7
// overrides
replace (
cosmossdk.io/core => cosmossdk.io/core v0.11.0
cosmossdk.io/store => github.com/evmos/cosmos-sdk/store v0.0.0-20240718141609-414cbd051fbe
github.com/CosmWasm/wasmd => github.com/rollchains/wasmd v0.50.0-evm
github.com/cosmos/cosmos-sdk => github.com/strangelove-ventures/cosmos-sdk v0.0.0-20250317212103-0767f8c5b1e5
github.com/cosmos/evm => github.com/strangelove-ventures/cosmos-evm v0.2.0
github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc2
github.com/sonr-io/sonr => ../../
github.com/sonr-io/sonr/crypto => ../../crypto
github.com/spf13/viper => github.com/spf13/viper v1.17.0 // v1.18+ breaks app overrides
nhooyr.io/websocket => nhooyr.io/websocket v1.8.17
)
replace (
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
// Fix btcec version for evmos compatibility
github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.2
// dgrijalva/jwt-go is deprecated and doesn't receive security updates.
// See: https://github.com/cosmos/cosmos-sdk/issues/13134
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
// Fix upstream GHSA-h395-qcrw-5vmq vulnerability.
// See: https://github.com/cosmos/cosmos-sdk/issues/10409
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.8.1
// pin version! 126854af5e6d has issues with the store so that queries fail
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/tyler-smith/go-bip39 => github.com/go-sonr/go-bip39 v1.1.1
)
require (
cosmossdk.io/log v1.5.0
cosmossdk.io/tools/confix v0.1.2
github.com/cometbft/cometbft v0.38.17
github.com/cosmos/cosmos-db v1.1.1
github.com/cosmos/cosmos-sdk v0.53.4
github.com/cosmos/evm v0.1.0
github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000
github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
)
require (
cosmossdk.io/api v0.7.6 // indirect
cosmossdk.io/client/v2 v2.0.0-beta.7 // indirect
cosmossdk.io/collections v0.4.0 // indirect
cosmossdk.io/core v0.12.0 // indirect
cosmossdk.io/depinject v1.1.0 // indirect
cosmossdk.io/errors v1.0.1 // indirect
cosmossdk.io/math v1.5.0 // indirect
cosmossdk.io/orm v1.0.0-beta.3 // indirect
cosmossdk.io/store v1.1.1 // indirect
cosmossdk.io/x/circuit v0.1.1 // indirect
cosmossdk.io/x/evidence v0.1.1 // indirect
cosmossdk.io/x/feegrant v0.1.1 // indirect
cosmossdk.io/x/nft v0.1.0 // indirect
cosmossdk.io/x/tx v0.13.7 // indirect
cosmossdk.io/x/upgrade v0.1.4 // indirect
github.com/CosmWasm/wasmvm v1.5.8 // indirect
github.com/Oudwins/zog v0.21.6 // indirect
github.com/asynkron/protoactor-go v0.0.0-20240822202345-3c0e61ca19c9 // indirect
github.com/biter777/countries v1.7.5 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogoproto v1.7.0 // indirect
github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8 v8.1.1 // indirect
github.com/cosmos/ibc-apps/modules/rate-limiting/v8 v8.0.0 // indirect
github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect
github.com/cosmos/ibc-go/modules/light-clients/08-wasm v0.1.1-0.20231213092650-57fcdb9a9a9d // indirect
github.com/cosmos/ibc-go/v8 v8.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/ethereum/go-ethereum v1.16.3 // indirect
github.com/extism/go-sdk v1.7.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/ipfs/boxo v0.32.0 // indirect
github.com/ipfs/kubo v0.35.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/echo/v4 v4.13.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/sonr-io/sonr/crypto v0.0.0-00010101000000-000000000000 // indirect
github.com/strangelove-ventures/tokenfactory v0.50.3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect
gorm.io/gorm v1.30.1 // indirect
nhooyr.io/websocket v1.8.10 // indirect
)
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.6.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.1.9 // indirect
cloud.google.com/go/storage v1.41.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.1 // indirect
github.com/DataDog/datadog-go v3.2.0+incompatible // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/VictoriaMetrics/fastcache v1.6.0 // indirect
github.com/Workiva/go-datastructures v1.1.3 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/alexvec/go-bip39 v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/bwesterb/go-ristretto v1.2.3 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/caddyserver/certmagic v0.21.6 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cockroachdb/apd/v2 v2.0.2 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/cockroachdb/crlib v0.0.0-20241015224233-894974b3ad94 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v1.1.2 // indirect
github.com/cockroachdb/pebble/v2 v2.0.3 // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/swiss v0.0.0-20250624142022-d6e517c1d961 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/cometbft/cometbft-db v0.14.1 // indirect
github.com/consensys/gnark-crypto v0.19.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/iavl v1.2.2 // indirect
github.com/cosmos/ics23/go v0.11.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect
github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect
github.com/creachadair/atomicfile v0.3.1 // indirect
github.com/creachadair/tomledit v0.0.24 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emicklei/dot v1.6.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gammazero/deque v1.0.0 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gogo/status v1.1.0 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/orderedcode v0.0.1 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-getter v1.7.9 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.3 // indirect
github.com/hashicorp/go-plugin v1.5.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/huandu/skiplist v1.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/iancoleman/orderedmap v0.3.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-bitfield v1.1.0 // indirect
github.com/ipfs/go-block-format v0.2.1 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/ipfs/go-datastore v0.8.2 // indirect
github.com/ipfs/go-ds-measure v0.2.2 // indirect
github.com/ipfs/go-fs-lock v0.1.1 // indirect
github.com/ipfs/go-ipfs-cmds v0.14.1 // indirect
github.com/ipfs/go-ipld-cbor v0.2.0 // indirect
github.com/ipfs/go-ipld-format v0.6.1 // indirect
github.com/ipfs/go-ipld-legacy v0.2.1 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.6.0 // indirect
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
github.com/ipfs/go-unixfsnode v1.10.1 // indirect
github.com/ipld/go-car/v2 v2.14.3 // indirect
github.com/ipld/go-codec-dagpb v1.7.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/ipshipyard/p2p-forge v0.5.1 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.0.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
github.com/libp2p/go-libp2p v0.43.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.33.1 // indirect
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/linxGnu/grocksdb v1.9.8 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0 // indirect
github.com/lmittmann/tint v1.0.3 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
github.com/minio/highwayhash v1.0.3 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.16.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.1 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.1 // indirect
github.com/multiformats/go-varint v0.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/orcaman/concurrent-map v1.0.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.19 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.6 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pion/webrtc/v4 v4.1.2 // 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/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/tsdb v0.10.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/quic-go/webtransport-go v0.9.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rjeczalik/notify v0.9.3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect
github.com/whyrusleeping/cbor-gen v0.1.2 // indirect
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v0.14.3 // indirect
go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/dig v1.19.0 // indirect
go.uber.org/fx v1.24.0 // indirect
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.186.0 // indirect
google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
pgregory.net/rapid v1.1.0 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
)
-18
View File
@@ -1,18 +0,0 @@
[tool.commitizen]
name = "cz_customize"
tag_format = "vault/v$version"
ignored_tag_formats = ["*/v${version}", "v${version}"]
version_scheme = "semver"
version_provider = "scm"
update_changelog_on_bump = true
changelog_file = "CHANGELOG.md"
major_version_zero = true
annotated_tag = true
pre_bump_hooks = ["bash scripts/hook-bump-pre.sh"]
post_bump_hooks = ["goreleaser release --clean -f cmd/vault/.goreleaser.yml"]
[tool.commitizen.customize]
bump_pattern = "^(feat|fix|refactor|perf|BREAKING CHANGE)"
bump_map = { "BREAKING CHANGE" = "MAJOR", "feat" = "MINOR", "fix" = "PATCH", "refactor" = "PATCH", "perf" = "PATCH" }
default_bump = "PATCH"
changelog_pattern = "^(feat|fix|refactor|docs|build)\\(vault\\)(!)?:"
-71
View File
@@ -1,71 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
---
version: 2
dist: dist/vault
monorepo:
tag_prefix: vault/
dir: cmd/vault
project_name: vault
before:
hooks:
- go mod download
builds:
- id: vault
main: main.go
binary: vault
no_unique_dist_dir: true
hooks:
post:
- cp dist/vault/vault.wasm x/dwn/client/plugin/vault.wasm
- cp dist/vault/vault.wasm packages/es/src/plugin/plugin.wasm
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- wasip1
goarch:
- wasm
flags:
- -mod=readonly
- -trimpath
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
archives:
- id: vault-wasm-archive
name_template: "vault_wasm_{{ .Version }}"
formats: ["binary"]
wrap_in_directory: true
blobs:
- provider: s3
endpoint: https://eb37925850388bca807b7fab964c12bb.r2.cloudflarestorage.com
bucket: releases
region: auto
directory: "vault/{{ .Tag }}"
release:
disable: false
github:
owner: sonr-io
name: sonr
name_template: "{{.ProjectName}}/{{ .Tag }}"
draft: false
replace_existing_draft: false # Don't replace drafts
replace_existing_artifacts: false # Append, don't replace
mode: append # Explicitly set to append mode
checksum:
name_template: "vault_checksums.txt"
snapshot:
version_template: "{{ incpatch .Version }}-dev"
# Changelog configuration
changelog:
sort: asc
-6
View File
@@ -1,6 +0,0 @@
## vault/v0.0.2 (2025-10-04)
## vault/v0.0.1 (2025-10-03)
- feat(vault): introduce assembly of the initial vault
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/make -f
# Output configuration - dual output for both plugin and ES package
GIT_ROOT := $(shell git rev-parse --show-toplevel)
PLUGIN_DIR := $(GIT_ROOT)/x/dwn/client/plugin
ES_PACKAGE_DIR := $(GIT_ROOT)/packages/es/src/plugin
OUTPUT_FILE := plugin.wasm
PLUGIN_PATH := $(PLUGIN_DIR)/vault.wasm
ES_PATH := $(ES_PACKAGE_DIR)/$(OUTPUT_FILE)
# Build configuration for WASM
GOOS := wasip1
GOARCH := wasm
# Version information
VERSION := $(shell echo $(shell git describe --tags 2>/dev/null || echo "dev") | sed 's/^v//')
COMMIT := $(shell git log -1 --format='%H')
.PHONY: all build clean install test version help
all: build
build:
@echo "Building vault WASM module..."
@echo "Targets: Plugin and ES package"
@mkdir -p $(PLUGIN_DIR)
@mkdir -p $(ES_PACKAGE_DIR)
@GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(PLUGIN_PATH) main.go
@cp $(PLUGIN_PATH) $(ES_PATH)
@echo "✅ Vault WASM module built successfully"
@echo "Plugin output: $(PLUGIN_PATH)"
@echo "ES package output: $(ES_PATH)"
@ls -lh $(PLUGIN_PATH) | awk '{print "Plugin size: " $$5}'
@ls -lh $(ES_PATH) | awk '{print "ES package size: " $$5}'
tidy:
@echo "Tidying vault build artifacts..."
@go mod tidy
@echo "✅ Tidy complete"
test:
@echo "Running vault tests..."
@go test -v ./...
@cd $(GIT_ROOT) && go test -C . -mod=readonly -v github.com/sonr-io/sonr/x/dwn/client/plugin/...
clean:
@echo "Cleaning vault build artifacts..."
@rm -f $(PLUGIN_PATH)
@rm -f $(ES_PATH)
@echo "✅ Clean complete"
release:
@echo "Creating vault release..."
@cd $(GIT_ROOT) && cz --config cmd/vault/.cz.toml --no-raise 6,21 bump --yes --increment PATCH
snapshot:
@echo "Dry-Run Bumping Vault version..."
@cd $(GIT_ROOT) && cz --config cmd/vault/.cz.toml bump --yes --dry-run --no-verify --increment PATCH
@echo "Creating vault snapshots for all platforms..."
@cd $(GIT_ROOT) && goreleaser release --snapshot --clean -f cmd/vault/.goreleaser.yml
version:
@echo "Vault WASM Module"
@echo "================="
@echo "Version: $(VERSION)"
@echo "Commit: $(COMMIT)"
@echo "Target OS: $(GOOS)"
@echo "Target Arch: $(GOARCH)"
@echo "Plugin output: $(PLUGIN_PATH)"
@echo "ES output: $(ES_PATH)"
verify: build
@echo "Verifying WASM modules..."
@if [ -f "$(PLUGIN_PATH)" ]; then \
file "$(PLUGIN_PATH)"; \
echo "✅ Plugin WASM file exists"; \
else \
echo "❌ Plugin WASM file not found"; \
exit 1; \
fi
@if [ -f "$(ES_PATH)" ]; then \
file "$(ES_PATH)"; \
echo "✅ ES package WASM file exists"; \
else \
echo "❌ ES package WASM file not found"; \
exit 1; \
fi
@echo "Plugin size: $$(du -h $(PLUGIN_PATH) | cut -f1)"
@echo "ES size: $$(du -h $(ES_PATH) | cut -f1)"
@echo "✅ Verification complete"
help:
@echo "Vault WASM Module Makefile"
@echo "=========================="
@echo ""
@echo "This Makefile builds the vault WebAssembly module for both the"
@echo "Sonr blockchain plugin system and the @sonr.io/es package."
@echo ""
@echo "Available targets:"
@echo " build - Build vault WASM module (default)"
@echo " clean - Remove built WASM artifacts"
@echo " test - Run vault tests"
@echo " tidy - Tidy Go module dependencies"
@echo " version - Display version information"
@echo " verify - Build and verify the WASM modules"
@echo " help - Show this help message"
@echo ""
@echo "Output locations:"
@echo " Plugin: $(PLUGIN_PATH)"
@echo " ES Package: $(ES_PATH)"
@echo ""
@echo "Integration:"
@echo " The WASM module is built for two purposes:"
@echo " 1. Plugin system in x/dwn/client/plugin"
@echo " 2. ES package for browser distribution via jsDelivr"
@echo ""
@echo "Examples:"
@echo " make build # Build WASM module to both locations"
@echo " make verify # Build and verify both outputs"
@echo " make clean # Remove all artifacts"
-477
View File
@@ -1,477 +0,0 @@
# Vault - WebAssembly Vault Plugin
Vault is a WebAssembly-based vault system for the Sonr blockchain that provides secure, isolated execution of cryptographic operations. Built using the Extism framework, Vault enables secure multi-party computation (MPC) and vault management within a sandboxed WebAssembly environment.
## Overview
Vault serves as a cryptographic vault system that:
- Provides secure enclave-based key generation and management
- Supports multi-chain transaction signing (Cosmos, EVM)
- Implements WebAuthn-based authentication
- Offers secure import/export functionality via IPFS
- Enables isolated execution through WebAssembly
## Architecture
### Core Components
- **MPC Enclave**: Multi-party computation system for secure key operations
- **Vault Management**: Create, unlock, and manage cryptographic vaults
- **IPFS Integration**: Secure backup and restore of encrypted vault data
- **WebAuthn Support**: Passwordless authentication for vault operations
- **Multi-Chain Support**: Transaction signing for different blockchain networks
### Build Configuration
Vault is built specifically for WebAssembly:
```go
//go:build js && wasm
// +build js,wasm
```
## API Reference
### Core Enclave Operations
#### `generate`
```go
//go:wasmexport generate
func generate() int32
```
Creates a new MPC enclave and returns the enclave data and public key.
**Input**: `GenerateRequest`
```json
{
"id": "string"
}
```
**Output**: `GenerateResponse`
```json
{
"data": "EnclaveData",
"public_key": "[]byte"
}
```
#### `refresh`
```go
//go:wasmexport refresh
func refresh() int32
```
Refreshes an existing enclave with new cryptographic material.
**Input**: `RefreshRequest`
```json
{
"enclave": "EnclaveData"
}
```
**Output**: `RefreshResponse`
```json
{
"okay": "bool",
"data": "EnclaveData"
}
```
#### `sign`
```go
//go:wasmexport sign
func sign() int32
```
Signs a message using the enclave's private key.
**Input**: `SignRequest`
```json
{
"message": "[]byte",
"enclave": "EnclaveData"
}
```
**Output**: `SignResponse`
```json
{
"signature": "[]byte"
}
```
#### `verify`
```go
//go:wasmexport verify
func verify() int32
```
Verifies a signature against a message and public key.
**Input**: `VerifyRequest`
```json
{
"public_key": "[]byte",
"message": "[]byte",
"signature": "[]byte"
}
```
**Output**: `VerifyResponse`
```json
{
"valid": "bool"
}
```
### Vault Import/Export Operations
#### `export`
```go
//go:wasmexport export
func export() int32
```
Encrypts and exports vault data to IPFS, returning a Content ID (CID).
**Input**: `ExportRequest`
```json
{
"enclave": "EnclaveData",
"password": "[]byte"
}
```
**Output**: `ExportResponse`
```json
{
"cid": "string",
"success": "bool"
}
```
#### `import`
```go
//go:wasmexport import
func importVault() int32
```
Retrieves and decrypts vault data from IPFS using a CID and password.
**Input**: `ImportRequest`
```json
{
"cid": "string",
"password": "[]byte"
}
```
**Output**: `ImportResponse`
```json
{
"enclave": "EnclaveData",
"success": "bool"
}
```
### Advanced Vault Operations
#### `create_vault_enclave`
```go
//go:wasmexport create_vault_enclave
func createVaultEnclave() int32
```
Creates a new vault enclave with advanced configuration options.
**Input**: `EnclaveConfig`
```json
{
"vault_id": "string",
"key_derivation_method": "string",
"encryption_algorithm": "string",
"signing_algorithm": "string",
"webauthn_enabled": "bool",
"auto_lock_timeout": "int64",
"key_rotation_interval": "int64",
"supported_chains": ["string"],
"max_concurrent_ops": "int",
"memory_limit": "uint64"
}
```
#### `unlock_vault_enclave`
```go
//go:wasmexport unlock_vault_enclave
func unlockVaultEnclave() int32
```
Unlocks a vault enclave, optionally using WebAuthn authentication.
#### `lock_vault_enclave`
```go
//go:wasmexport lock_vault_enclave
func lockVaultEnclave() int32
```
Locks a vault enclave to prevent unauthorized access.
#### `rotate_vault_key`
```go
//go:wasmexport rotate_vault_key
func rotateVaultKey() int32
```
Rotates the cryptographic keys within a vault enclave.
### Multi-Chain Transaction Signing
#### `sign_cosmos_transaction`
```go
//go:wasmexport sign_cosmos_transaction
func signCosmosTransaction() int32
```
Signs transactions for Cosmos SDK-based blockchains.
#### `sign_evm_transaction`
```go
//go:wasmexport sign_evm_transaction
func signEvmTransaction() int32
```
Signs transactions for Ethereum Virtual Machine compatible chains.
#### `sign_message`
```go
//go:wasmexport sign_message
func signMessage() int32
```
Signs arbitrary messages using the vault's private key.
### Health and Monitoring
#### `get_vault_health`
```go
//go:wasmexport get_vault_health
func getVaultHealth() int32
```
Returns the health status of a vault enclave.
**Output**: `EnclaveHealth`
```json
{
"vault_id": "string",
"status": "string",
"last_activity": "int64",
"key_rotation_due": "bool",
"attestation_valid": "bool"
}
```
## Configuration
### Environment Variables
Motor supports configuration through Extism variables:
- `chain_id`: Blockchain network identifier (default: "sonr-testnet-1")
- `password`: Default password for enclave operations (default: "password")
- `gateway`: IPFS gateway URL (default: "https://ipfs.did.run/ipfs/")
Access these via helper functions:
```go
func GetChainID() string
func GetPassword() []byte
func GetGateway() string
```
### IPFS Integration
Motor integrates with IPFS for secure vault backup and restore:
- **Storage Endpoint**: `http://127.0.0.1:5001/api/v0/add`
- **Retrieval Endpoint**: `http://127.0.0.1:5001/api/v0/cat`
- **Data Format**: Encrypted vault data stored as content-addressed objects
- **Security**: All vault data is encrypted before IPFS storage
## Security Features
### Enclave Isolation
- WebAssembly sandbox provides memory isolation
- Secure execution environment prevents side-channel attacks
- Attestation mechanisms ensure enclave integrity
### Authentication
- WebAuthn support for passwordless authentication
- Challenge-response authentication flows
- Automatic vault locking with configurable timeouts
### Key Management
- Multi-party computation for enhanced security
- Automatic key rotation with configurable intervals
- Secure key derivation and storage
### Data Protection
- AES encryption for sensitive data
- Password-based encryption for import/export
- Secure memory handling within WASM environment
## Usage Examples
### Basic Enclave Operations
```javascript
// Generate new enclave
const generateReq = { id: "my-vault" };
const result = call_wasm_function("generate", generateReq);
// Sign a message
const signReq = {
message: new Uint8Array([1, 2, 3, 4]),
enclave: result.data,
};
const signature = call_wasm_function("sign", signReq);
```
### Vault Management
```javascript
// Create vault with configuration
const config = {
vault_id: "user-vault-001",
webauthn_enabled: true,
auto_lock_timeout: 300,
supported_chains: ["cosmos", "ethereum"],
};
const vault = call_wasm_function("create_vault_enclave", config);
// Sign Cosmos transaction
const cosmosReq = {
vault_id: "user-vault-001",
chain_type: "cosmos",
chain_id: "cosmoshub-4",
message: transactionBytes,
};
const cosmosResult = call_wasm_function("sign_cosmos_transaction", cosmosReq);
```
### Import/Export Operations
```javascript
// Export vault to IPFS
const exportReq = {
enclave: vaultData,
password: new Uint8Array([
/* password bytes */
]),
};
const exportResult = call_wasm_function("export", exportReq);
console.log("Vault exported to CID:", exportResult.cid);
// Import vault from IPFS
const importReq = {
cid: "QmXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx",
password: new Uint8Array([
/* password bytes */
]),
};
const importResult = call_wasm_function("import", importReq);
```
## Building and Deployment
### Prerequisites
- Go 1.24.4+
- Extism runtime
- IPFS node (for import/export functionality)
### Build Commands
```bash
# Build WebAssembly module
GOOS=js GOARCH=wasm go build -o motr.wasm main.go
# Build via Makefile
make motr
```
### Integration
Motor is designed to be integrated with:
- **Highway Service**: PostgreSQL-backed HTTP API
- **Sonr Blockchain**: Cosmos SDK-based blockchain node
- **IPFS Network**: Decentralized storage system
- **WebAuthn Infrastructure**: Passwordless authentication
## Error Handling
All functions return `int32` status codes:
- `0`: Success
- `1`: Error (details available via `pdk.SetError`)
Error information is logged using Extism's logging system:
```go
pdk.Log(pdk.LogError, "Error message")
pdk.Log(pdk.LogInfo, "Info message")
```
## Dependencies
### Core Dependencies
- `github.com/extism/go-pdk`: WebAssembly plugin development kit
- `github.com/sonr-io/sonr/crypto/mpc`: Multi-party computation library
### Cryptographic Libraries
- `filippo.io/edwards25519`: Edwards25519 elliptic curve
- `github.com/btcsuite/btcd/btcec/v2`: Bitcoin cryptography
- `github.com/consensys/gnark-crypto`: Zero-knowledge proof cryptography
## License
Motor is part of the Sonr blockchain project. See the project's main license for terms and conditions.
-26
View File
@@ -1,26 +0,0 @@
module vault
go 1.24.7
replace github.com/sonr-io/sonr/crypto => ../../crypto/
require (
github.com/extism/go-pdk v1.1.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/sonr-io/sonr/crypto v0.0.0-00010101000000-000000000000
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // 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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)
-38
View File
@@ -1,38 +0,0 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
github.com/bits-and-blooms/bitset v1.24.0/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/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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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/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/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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-483
View File
@@ -1,483 +0,0 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"crypto/sha256"
"encoding/json"
"fmt"
"time"
"github.com/extism/go-pdk"
"github.com/golang-jwt/jwt/v5"
"github.com/sonr-io/sonr/crypto/mpc"
)
const (
KeyChainID = "chain_id"
KeyEnclave = "enclave"
KeyVaultConfig = "vault_config"
)
// GetChainID returns the chain ID to use for unlocking the enclave
func GetChainID() string {
v := pdk.GetVar(KeyChainID)
if v == nil {
return "sonr-testnet-1"
}
return string(v)
}
// GetEnclaveData loads MPC enclave data from PDK environment
func GetEnclaveData() (*mpc.EnclaveData, error) {
v := pdk.GetVar(KeyEnclave)
if v == nil {
return nil, fmt.Errorf("enclave data not provided in environment")
}
var data mpc.EnclaveData
if err := json.Unmarshal(v, &data); err != nil {
return nil, fmt.Errorf("failed to unmarshal enclave data: %w", err)
}
return &data, nil
}
// GetVaultConfig loads vault configuration from PDK environment
func GetVaultConfig() map[string]any {
v := pdk.GetVar(KeyVaultConfig)
if v == nil {
return make(map[string]any)
}
var config map[string]any
if err := json.Unmarshal(v, &config); err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to parse vault config: %v", err))
return make(map[string]any)
}
return config
}
// UCAN Token Request/Response types
type NewOriginTokenRequest struct {
AudienceDID string `json:"audience_did"`
Attenuations []map[string]any `json:"attenuations,omitempty"`
Facts []string `json:"facts,omitempty"`
NotBefore int64 `json:"not_before,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
type NewAttenuatedTokenRequest struct {
ParentToken string `json:"parent_token"`
AudienceDID string `json:"audience_did"`
Attenuations []map[string]any `json:"attenuations,omitempty"`
Facts []string `json:"facts,omitempty"`
NotBefore int64 `json:"not_before,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
type UCANTokenResponse struct {
Token string `json:"token"`
Issuer string `json:"issuer"`
Address string `json:"address"`
Error string `json:"error,omitempty"`
}
type SignDataRequest struct {
Data []byte `json:"data"`
}
type SignDataResponse struct {
Signature []byte `json:"signature"`
Error string `json:"error,omitempty"`
}
type VerifyDataRequest struct {
Data []byte `json:"data"`
Signature []byte `json:"signature"`
}
type VerifyDataResponse struct {
Valid bool `json:"valid"`
Error string `json:"error,omitempty"`
}
type GetIssuerDIDResponse struct {
IssuerDID string `json:"issuer_did"`
Address string `json:"address"`
ChainCode string `json:"chain_code"`
Error string `json:"error,omitempty"`
}
var (
enclave mpc.Enclave
issuerDID string
address string
)
func main() {
// Initialize MPC enclave from PDK environment
if err := initializeEnclave(); err != nil {
pdk.SetError(fmt.Errorf("failed to initialize enclave: %w", err))
return
}
pdk.Log(pdk.LogInfo, "Motor plugin initialized as MPC-based UCAN source")
}
// initializeEnclave initializes the MPC enclave from PDK environment
func initializeEnclave() error {
// Load enclave data from PDK environment
enclaveData, err := GetEnclaveData()
if err != nil {
return fmt.Errorf("failed to get enclave data: %w", err)
}
// Import MPC enclave from data
enclave, err = mpc.ImportEnclave(mpc.WithEnclaveData(enclaveData))
if err != nil {
return fmt.Errorf("failed to import enclave: %w", err)
}
// Derive issuer DID and address from enclave public key
pubKeyBytes := enclave.PubKeyBytes()
issuerDID, address, err = deriveIssuerDIDFromBytes(pubKeyBytes)
if err != nil {
return fmt.Errorf("failed to derive issuer DID: %w", err)
}
return nil
}
//go:wasmexport new_origin_token
func newOriginToken() int32 {
if !enclave.IsValid() {
pdk.SetError(fmt.Errorf("enclave not initialized"))
return 1
}
req := &NewOriginTokenRequest{}
err := pdk.InputJSON(req)
if err != nil {
pdk.SetError(fmt.Errorf("failed to parse request: %w", err))
return 1
}
// Convert timestamps
var notBefore, expiresAt time.Time
if req.NotBefore > 0 {
notBefore = time.Unix(req.NotBefore, 0)
}
if req.ExpiresAt > 0 {
expiresAt = time.Unix(req.ExpiresAt, 0)
}
// Create origin token using MPC signing
tokenString, err := createUCANToken(
req.AudienceDID,
nil,
req.Attenuations,
req.Facts,
notBefore,
expiresAt,
)
if err != nil {
resp := &UCANTokenResponse{Error: err.Error()}
pdk.OutputJSON(resp)
return 1
}
resp := &UCANTokenResponse{
Token: tokenString,
Issuer: issuerDID,
Address: address,
}
pdk.OutputJSON(resp)
return 0
}
//go:wasmexport new_attenuated_token
func newAttenuatedToken() int32 {
if !enclave.IsValid() {
pdk.SetError(fmt.Errorf("enclave not initialized"))
return 1
}
req := &NewAttenuatedTokenRequest{}
err := pdk.InputJSON(req)
if err != nil {
pdk.SetError(fmt.Errorf("failed to parse request: %w", err))
return 1
}
// Convert timestamps
var notBefore, expiresAt time.Time
if req.NotBefore > 0 {
notBefore = time.Unix(req.NotBefore, 0)
}
if req.ExpiresAt > 0 {
expiresAt = time.Unix(req.ExpiresAt, 0)
}
// Create proofs from parent token
proofs := []string{req.ParentToken}
// Create attenuated token using MPC signing
tokenString, err := createUCANToken(
req.AudienceDID,
proofs,
req.Attenuations,
req.Facts,
notBefore,
expiresAt,
)
if err != nil {
resp := &UCANTokenResponse{Error: err.Error()}
pdk.OutputJSON(resp)
return 1
}
resp := &UCANTokenResponse{
Token: tokenString,
Issuer: issuerDID,
Address: address,
}
pdk.OutputJSON(resp)
return 0
}
//go:wasmexport sign_data
func signData() int32 {
if !enclave.IsValid() {
pdk.SetError(fmt.Errorf("enclave not initialized"))
return 1
}
req := &SignDataRequest{}
err := pdk.InputJSON(req)
if err != nil {
pdk.SetError(fmt.Errorf("failed to parse request: %w", err))
return 1
}
// Sign data using MPC enclave
signature, err := enclave.Sign(req.Data)
if err != nil {
resp := &SignDataResponse{Error: err.Error()}
pdk.OutputJSON(resp)
return 1
}
resp := &SignDataResponse{Signature: signature}
pdk.OutputJSON(resp)
return 0
}
//go:wasmexport verify_data
func verifyData() int32 {
if !enclave.IsValid() {
pdk.SetError(fmt.Errorf("enclave not initialized"))
return 1
}
req := &VerifyDataRequest{}
err := pdk.InputJSON(req)
if err != nil {
pdk.SetError(fmt.Errorf("failed to parse request: %w", err))
return 1
}
// Verify data using MPC enclave
valid, err := enclave.Verify(req.Data, req.Signature)
if err != nil {
resp := &VerifyDataResponse{Error: err.Error()}
pdk.OutputJSON(resp)
return 1
}
resp := &VerifyDataResponse{Valid: valid}
pdk.OutputJSON(resp)
return 0
}
//go:wasmexport get_issuer_did
func getIssuerDID() int32 {
if !enclave.IsValid() {
pdk.SetError(fmt.Errorf("enclave not initialized"))
return 1
}
// Get chain code for deterministic derivation
chainCode, err := getChainCode()
if err != nil {
resp := &GetIssuerDIDResponse{Error: err.Error()}
pdk.OutputJSON(resp)
return 1
}
resp := &GetIssuerDIDResponse{
IssuerDID: issuerDID,
Address: address,
ChainCode: fmt.Sprintf("%x", chainCode),
}
pdk.OutputJSON(resp)
return 0
}
// UCAN token creation and MPC signing implementation
// MPCSigningMethod implements JWT signing using MPC enclaves
type MPCSigningMethod struct {
Name string
enclave mpc.Enclave
}
// Alg returns the signing method algorithm name
func (m *MPCSigningMethod) Alg() string {
return m.Name
}
// Sign signs a JWT string using the MPC enclave
func (m *MPCSigningMethod) Sign(signingString string, key any) ([]byte, error) {
// Hash the signing string
hasher := sha256.New()
hasher.Write([]byte(signingString))
digest := hasher.Sum(nil)
// Use MPC enclave to sign the digest
sig, err := m.enclave.Sign(digest)
if err != nil {
return nil, fmt.Errorf("failed to sign with MPC: %w", err)
}
return sig, nil
}
// Verify verifies a JWT signature using the MPC enclave
func (m *MPCSigningMethod) Verify(signingString string, sig []byte, key any) error {
// Hash the signing string
hasher := sha256.New()
hasher.Write([]byte(signingString))
digest := hasher.Sum(nil)
// Use MPC enclave to verify signature
valid, err := m.enclave.Verify(digest, sig)
if err != nil {
return fmt.Errorf("failed to verify signature: %w", err)
}
if !valid {
return fmt.Errorf("signature verification failed")
}
return nil
}
// createUCANToken creates a UCAN token using MPC signing
func createUCANToken(
audienceDID string,
proofs []string,
attenuations []map[string]any,
facts []string,
notBefore, expiresAt time.Time,
) (string, error) {
// Validate audience DID
if audienceDID == "" {
return "", fmt.Errorf("audience DID is required")
}
// Create MPC signing method
signingMethod := &MPCSigningMethod{
Name: "MPC256",
enclave: enclave,
}
// Create JWT token
token := jwt.New(signingMethod)
// Set UCAN version in header
token.Header["ucv"] = "0.9.0"
// Prepare time claims
var nbfUnix, expUnix int64
if !notBefore.IsZero() {
nbfUnix = notBefore.Unix()
}
if !expiresAt.IsZero() {
expUnix = expiresAt.Unix()
}
// Set claims
claims := jwt.MapClaims{
"iss": issuerDID,
"aud": audienceDID,
}
// Add attenuations if provided
if len(attenuations) > 0 {
claims["att"] = attenuations
}
// Add proofs if provided
if len(proofs) > 0 {
claims["prf"] = proofs
}
// Add facts if provided
if len(facts) > 0 {
claims["fct"] = facts
}
// Add time claims
if nbfUnix > 0 {
claims["nbf"] = nbfUnix
}
if expUnix > 0 {
claims["exp"] = expUnix
}
token.Claims = claims
// Sign the token using MPC enclave (key parameter is ignored for MPC signing)
tokenString, err := token.SignedString(nil)
if err != nil {
return "", fmt.Errorf("failed to sign token with MPC: %w", err)
}
return tokenString, nil
}
// deriveIssuerDIDFromBytes creates issuer DID and address from public key bytes
func deriveIssuerDIDFromBytes(pubKeyBytes []byte) (string, string, error) {
if len(pubKeyBytes) == 0 {
return "", "", fmt.Errorf("empty public key bytes")
}
// Generate address from public key (simplified implementation)
address := fmt.Sprintf("sonr1%x", pubKeyBytes[:20])
// Create DID from address (simplified implementation)
issuerDID := fmt.Sprintf("did:sonr:%s", address)
return issuerDID, address, nil
}
// getChainCode derives a deterministic chain code from the enclave
func getChainCode() ([]byte, error) {
if !enclave.IsValid() {
return nil, fmt.Errorf("enclave is not valid")
}
// Sign the address to create a deterministic chain code
sig, err := enclave.Sign([]byte(address))
if err != nil {
return nil, fmt.Errorf("failed to sign address for chain code: %w", err)
}
// Hash the signature to create a 32-byte chain code
hasher := sha256.New()
hasher.Write(sig)
hash := hasher.Sum(nil)
return hash[:32], nil
}
-4
View File
@@ -1,4 +0,0 @@
package main
// Version is set by commitizen during release process
var Version = "dev"
-353
View File
@@ -1,353 +0,0 @@
# Wyoming DAO Compliance Verification
## Executive Summary
This document verifies that the Identity DAO implementation complies with Wyoming's Decentralized Autonomous Organization (DAO) legal framework under W.S. 17-31-101 through 17-31-116, ensuring the DAO can operate as a legally recognized entity.
## Wyoming DAO Requirements Checklist
### ✅ Formation Requirements (W.S. 17-31-104)
#### 1. Named Entity
**Requirement**: The DAO must have a publicly stated name
**Implementation**: ✅ Complete
```rust
// contracts/core/src/state.rs
pub struct Config {
pub dao_name: String, // "Sonr Identity DAO"
pub dao_uri: String, // "https://sonr.io/dao"
// ...
}
```
#### 2. Public Address
**Requirement**: Maintain a publicly available address
**Implementation**: ✅ Complete
- Smart contract addresses on Cosmos Hub are immutable and public
- Deployment addresses stored in `artifacts/addresses.json`
- Published in documentation and on-chain metadata
#### 3. Member Registry
**Requirement**: Maintain a record of members
**Implementation**: ✅ Complete
```rust
// contracts/voting/src/state.rs
pub const VOTERS: Map<&Addr, VoterInfo> = Map::new("voters");
pub const DID_REGISTRY: Map<&str, Addr> = Map::new("did_registry");
```
### ✅ Governance Requirements (W.S. 17-31-105)
#### 1. Voting Mechanism
**Requirement**: Clear voting procedures and member rights
**Implementation**: ✅ Complete
```rust
// contracts/voting/src/contract.rs
pub fn execute_vote(
deps: DepsMut,
env: Env,
info: MessageInfo,
proposal_id: u64,
vote: Vote,
) -> Result<Response, ContractError>
```
**Voting Rights Matrix**:
| Verification Level | Vote Weight | Proposal Rights |
|-------------------|-------------|-----------------|
| Basic (Level 1) | 1x | Standard |
| Advanced (Level 2) | 2x | Policy |
| Full (Level 3) | 3x | All |
#### 2. Proposal Process
**Requirement**: Defined proposal submission and execution process
**Implementation**: ✅ Complete
- Pre-proposal review system
- Time-bound voting periods
- Automatic execution of passed proposals
- Transparent proposal lifecycle
#### 3. Record Keeping
**Requirement**: Maintain governance records
**Implementation**: ✅ Complete
```rust
// All votes and proposals stored on-chain
pub const PROPOSALS: Map<u64, ProposalData> = Map::new("proposals");
pub const VOTES: Map<(u64, &Addr), VoteInfo> = Map::new("votes");
```
### ✅ Management Structure (W.S. 17-31-106)
#### 1. Algorithmically Managed
**Requirement**: Can be algorithmically managed through smart contracts
**Implementation**: ✅ Complete
- Fully autonomous operation via smart contracts
- No requirement for human intervention in normal operations
- Automatic proposal execution
#### 2. Administrator Rights
**Requirement**: Clear admin/management structure
**Implementation**: ✅ Complete
```rust
pub struct Config {
pub admin: Addr, // Can be DAO itself for full decentralization
pub proposal_modules: Vec<Addr>,
pub voting_module: Addr,
}
```
### ✅ Member Rights (W.S. 17-31-107)
#### 1. Inspection Rights
**Requirement**: Members can inspect DAO records
**Implementation**: ✅ Complete
- All data queryable on-chain
- Public query endpoints for all state
#### 2. Information Rights
**Requirement**: Access to DAO information
**Implementation**: ✅ Complete
```rust
// Query endpoints provide full transparency
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Config {} => to_json_binary(&query_config(deps)?),
QueryMsg::Proposal { id } => to_json_binary(&query_proposal(deps, id)?),
QueryMsg::ListProposals { ... } => to_json_binary(&query_proposals(deps, ...)?),
// ... all state queryable
}
}
```
#### 3. Withdrawal Rights
**Requirement**: Member withdrawal procedures
**Implementation**: ✅ Complete
- Members can withdraw pending proposals
- Refund mechanisms for deposits
- No locked voting periods
### ✅ Dispute Resolution (W.S. 17-31-108)
#### 1. On-Chain Resolution
**Requirement**: Dispute resolution mechanism
**Implementation**: ✅ Complete
- Governance proposals for dispute resolution
- Multi-signature approval for critical decisions
- Transparent voting on disputes
#### 2. Alternative Dispute Resolution
**Requirement**: May include arbitration clauses
**Implementation**: ✅ Complete
```rust
// Configurable dispute resolution via governance
pub enum ProposalMessage {
DisputeResolution {
case_id: String,
resolution: String,
affected_parties: Vec<Addr>,
},
// ...
}
```
### ✅ Operating Agreement (W.S. 17-31-109)
#### 1. Smart Contract as Operating Agreement
**Requirement**: Operating agreement can be algorithmic
**Implementation**: ✅ Complete
- Smart contract code serves as operating agreement
- Immutable rules unless upgraded via governance
- All terms encoded in contract logic
#### 2. Amendment Process
**Requirement**: Process for amending operating agreement
**Implementation**: ✅ Complete
```rust
// Migration support for contract upgrades
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
// Only through governance proposal
ensure_approved_by_governance(deps.as_ref(), &msg)?;
// ... migration logic
}
```
### ✅ Legal Capacity (W.S. 17-31-110)
#### 1. Contract Rights
**Requirement**: Ability to enter contracts
**Implementation**: ✅ Complete
- Can hold and transfer assets
- Can execute arbitrary CosmWasm messages
- Can interact with other contracts
#### 2. Legal Standing
**Requirement**: Sue and be sued
**Implementation**: ✅ Complete
- Named entity with public address
- Identifiable on-chain presence
- Governance-controlled treasury
### ✅ Taxation (W.S. 17-31-111)
#### 1. Tax Identification
**Requirement**: May need EIN for US operations
**Implementation**: ⚠️ Off-chain requirement
- Contract stores tax configuration if needed
- Governance can update tax parameters
#### 2. Tax Reporting
**Requirement**: Maintain records for tax purposes
**Implementation**: ✅ Complete
- All transactions recorded on-chain
- Exportable transaction history
- Treasury tracking
## Implementation Verification
### Core Compliance Features
```rust
// contracts/core/src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct WyomingDAOConfig {
pub entity_name: String, // ✅ Named entity
pub public_address: Addr, // ✅ Public address
pub formation_date: Timestamp, // ✅ Formation record
pub operating_agreement_uri: String, // ✅ Operating agreement
pub tax_classification: Option<String>, // ✅ Tax status
pub registered_agent: Option<String>, // ✅ Wyoming agent
}
// contracts/core/src/wyoming.rs
pub fn verify_wyoming_compliance(deps: Deps) -> StdResult<ComplianceStatus> {
let config = CONFIG.load(deps.storage)?;
Ok(ComplianceStatus {
has_name: !config.dao_name.is_empty(),
has_public_address: true, // Always true for contracts
has_member_registry: VOTERS.keys(deps.storage, None, None, Order::Ascending)
.count()? > 0,
has_voting_mechanism: config.voting_module != Addr::unchecked(""),
has_governance_records: true, // On-chain storage
has_dispute_resolution: true, // Via governance
compliant: true,
})
}
```
### Deployment Configuration
```json
{
"wyoming_dao_config": {
"entity_name": "Sonr Identity DAO LLC",
"formation_date": "2024-01-01T00:00:00Z",
"operating_agreement_uri": "ipfs://QmXxx...",
"tax_classification": "Partnership",
"registered_agent": "Wyoming Registered Agent LLC",
"registered_address": "30 N Gould St, Sheridan, WY 82801"
}
}
```
## Legal Integration Points
### 1. With Existing Sonr Governance
The Identity DAO integrates with Sonr's existing governance through:
- IBC channels for cross-chain proposals
- DID-based identity verification
- Shared treasury management protocols
### 2. With Cosmos Hub Governance
As deployed on Cosmos Hub:
- Respects Cosmos Hub governance parameters
- Can participate in Hub governance via IBC
- Maintains sovereign decision-making
## Compliance Monitoring
### On-Chain Metrics
```rust
pub fn query_compliance_metrics(deps: Deps) -> StdResult<ComplianceMetrics> {
Ok(ComplianceMetrics {
total_members: count_members(deps)?,
active_proposals: count_active_proposals(deps)?,
treasury_value: query_treasury_balance(deps)?,
last_activity: get_last_activity(deps)?,
formation_date: CONFIG.load(deps.storage)?.formation_date,
})
}
```
### Off-Chain Requirements
1. **Annual Report**: File with Wyoming Secretary of State
2. **Registered Agent**: Maintain Wyoming registered agent
3. **Tax Filings**: If applicable based on operations
4. **Legal Notices**: Serve through registered agent
## Risk Mitigation
### Legal Risks
| Risk | Mitigation |
|------|------------|
| Regulatory Uncertainty | Conservative compliance approach |
| Cross-Border Issues | Clear jurisdictional statements |
| Tax Obligations | Governance-controlled tax parameters |
| Liability Concerns | Limited liability through LLC structure |
### Technical Risks
| Risk | Mitigation |
|------|------------|
| Smart Contract Bugs | Comprehensive testing and audits |
| Governance Attacks | Sybil resistance via DID verification |
| IBC Failures | Fallback governance mechanisms |
## Recommendations
### Immediate Actions
1.**Register Wyoming LLC**: File with Wyoming Secretary of State
2.**Obtain EIN**: If US tax obligations exist
3.**Appoint Registered Agent**: Required for Wyoming entity
4.**Publish Operating Agreement**: Make smart contract addresses public
### Ongoing Compliance
1. **Annual Filings**: Maintain good standing in Wyoming
2. **Record Keeping**: Export on-chain data periodically
3. **Tax Compliance**: File required returns
4. **Legal Updates**: Monitor Wyoming DAO law changes
## Conclusion
The Identity DAO implementation **fully complies** with Wyoming DAO legal requirements as specified in W.S. 17-31-101 through 17-31-116. The smart contract architecture provides:
**Legal Recognition**: Meets all formation requirements
**Governance Structure**: Complete voting and proposal systems
**Member Rights**: Full transparency and participation
**Operational Autonomy**: Algorithmic management capability
**Legal Capacity**: Can conduct business as legal entity
The implementation is ready for deployment as a Wyoming DAO LLC, pending only the administrative filing requirements with the Wyoming Secretary of State.
## Appendix: Legal Resources
- [Wyoming DAO Law (SF0038)](https://www.wyoleg.gov/2021/Introduced/SF0038.pdf)
- [Wyoming Secretary of State](https://sos.wyo.gov/)
- [DAO LLC Filing Instructions](https://sos.wyo.gov/business/FilingInstructions/DAO_FilingInstructions.pdf)
- [Registered Agent Services](https://www.wyomingagents.com/)
## Certification
This compliance verification was conducted based on the Identity DAO smart contract implementation as of the current version. Any modifications to the contracts should be reviewed for continued compliance.
**Prepared by**: Identity DAO Development Team
**Date**: 2024
**Version**: 1.0.0
**Chain**: Cosmos Hub (via IBC to Sonr)
-72
View File
@@ -1,72 +0,0 @@
[workspace]
resolver = "2"
members = [
"contracts/core",
"contracts/voting",
"contracts/proposals",
"contracts/pre-propose",
"packages/shared",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Sonr <dev@sonr.io>"]
license = "Apache-2.0"
repository = "https://github.com/sonr-io/sonr"
homepage = "https://sonr.io"
documentation = "https://sonr.dev"
[workspace.dependencies]
# CosmWasm dependencies
cosmwasm-std = { version = "2.1", features = ["stargate", "staking", "cosmwasm_2_0"] }
cosmwasm-schema = "2.1"
cw2 = "2.0"
cw-storage-plus = "2.0"
cw-utils = "2.0"
# DAO DAO dependencies for compatibility
dao-interface = "2.6"
dao-voting = "2.6"
dao-dao-macros = "2.6"
dao-pre-propose-base = "2.6"
# Serialization
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0"
schemars = "0.8"
thiserror = "2.0"
# Testing
cw-multi-test = "2.1"
anyhow = "1.0"
# Shared types
identity-dao-shared = { path = "packages/shared" }
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[profile.release.package.identity-dao-core]
codegen-units = 1
opt-level = 3
[profile.release.package.identity-dao-voting]
codegen-units = 1
opt-level = 3
[profile.release.package.identity-dao-proposals]
codegen-units = 1
opt-level = 3
[profile.release.package.identity-dao-pre-propose]
codegen-units = 1
opt-level = 3
-458
View File
@@ -1,458 +0,0 @@
# IBC Integration for Identity DAO
## Overview
The Identity DAO will be deployed on Cosmos Hub and communicate with Sonr chain modules via IBC (Inter-Blockchain Communication). This document outlines the IBC integration architecture and implementation details.
## Architecture
```mermaid
graph LR
subgraph "Cosmos Hub"
A[Identity DAO Contracts]
B[IBC Light Client]
end
subgraph "IBC Channel"
C[Relayer]
end
subgraph "Sonr Chain"
D[x/did Module]
E[x/dwn Module]
F[x/svc Module]
end
A -->|IBC Query| B
B <-->|Packets| C
C <-->|Packets| D
C <-->|Packets| E
C <-->|Packets| F
```
## IBC Channel Configuration
### Channel Setup
```bash
# Create IBC channel between Cosmos Hub and Sonr
hermes create channel \
--a-chain cosmoshub-4 \
--b-chain sonr-1 \
--a-port wasm.cosmos1... \
--b-port did \
--order unordered \
--version ics20-1
```
### Connection Parameters
```json
{
"channel_id": "channel-xxx",
"port_id": "wasm.cosmos1abc...",
"counterparty_channel_id": "channel-yyy",
"counterparty_port_id": "did",
"connection_id": "connection-xxx",
"version": "ics20-1"
}
```
## Contract Modifications for IBC
### 1. IBC Entry Points
Add IBC entry points to each contract:
```rust
// contracts/core/src/ibc.rs
use cosmwasm_std::{
entry_point, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg,
IbcChannelOpenMsg, IbcChannelOpenResponse, IbcPacketAckMsg, IbcPacketReceiveMsg,
IbcPacketTimeoutMsg, IbcReceiveResponse, Never,
};
#[entry_point]
pub fn ibc_channel_open(
_deps: DepsMut,
_env: Env,
msg: IbcChannelOpenMsg,
) -> Result<IbcChannelOpenResponse, ContractError> {
// Validate channel parameters
validate_channel(&msg.channel)?;
Ok(None)
}
#[entry_point]
pub fn ibc_channel_connect(
deps: DepsMut,
_env: Env,
msg: IbcChannelConnectMsg,
) -> Result<IbcBasicResponse, ContractError> {
// Store channel info
let channel = msg.channel();
CHANNEL_INFO.save(deps.storage, &channel.endpoint, &channel)?;
Ok(IbcBasicResponse::new()
.add_attribute("method", "ibc_channel_connect")
.add_attribute("channel_id", channel.endpoint.channel_id))
}
#[entry_point]
pub fn ibc_packet_receive(
deps: DepsMut,
env: Env,
msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, Never> {
// Handle incoming IBC packets
let packet = msg.packet;
let res = handle_packet(deps, env, packet)?;
Ok(IbcReceiveResponse::new()
.set_ack(res.acknowledgement)
.add_attributes(res.attributes))
}
```
### 2. Cross-Chain DID Queries
Replace Stargate queries with IBC queries:
```rust
// contracts/voting/src/ibc_query.rs
use cosmwasm_std::{to_binary, Binary, Deps, Env, IbcMsg, IbcTimeout};
pub fn query_did_via_ibc(
deps: Deps,
env: Env,
did: String,
) -> Result<IbcMsg, ContractError> {
let channel = CHANNEL_INFO.load(deps.storage)?;
let query_msg = DIDQuery {
query_type: "get_did_document",
did,
};
let packet = IbcMsg::SendPacket {
channel_id: channel.endpoint.channel_id,
data: to_binary(&query_msg)?,
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(60)),
};
Ok(packet)
}
pub fn handle_did_response(
deps: DepsMut,
response: Binary,
) -> Result<(), ContractError> {
let did_doc: DIDDocumentResponse = from_binary(&response)?;
// Cache DID document for voting power calculation
DID_CACHE.save(
deps.storage,
&did_doc.document.id,
&did_doc,
)?;
Ok(())
}
```
### 3. Asynchronous Voting Power
Since IBC queries are asynchronous, modify voting to handle this:
```rust
// contracts/voting/src/state.rs
pub struct PendingVote {
pub voter: Addr,
pub proposal_id: u64,
pub vote: Vote,
pub did_query_id: String,
}
pub const PENDING_VOTES: Map<String, PendingVote> = Map::new("pending_votes");
// contracts/voting/src/contract.rs
pub fn execute_vote(
deps: DepsMut,
env: Env,
info: MessageInfo,
proposal_id: u64,
vote: Vote,
) -> Result<Response, ContractError> {
// Check if we have cached DID info
let did = format!("did:sonr:{}", info.sender);
if let Some(did_doc) = DID_CACHE.may_load(deps.storage, &did)? {
// Process vote immediately
process_vote(deps, env, info.sender, proposal_id, vote, did_doc)
} else {
// Queue vote and query DID via IBC
let query_id = generate_query_id(&env, &info.sender);
PENDING_VOTES.save(
deps.storage,
&query_id,
&PendingVote {
voter: info.sender,
proposal_id,
vote,
did_query_id: query_id.clone(),
},
)?;
let ibc_msg = query_did_via_ibc(deps.as_ref(), env, did)?;
Ok(Response::new()
.add_message(ibc_msg)
.add_attribute("action", "vote_pending")
.add_attribute("query_id", query_id))
}
}
```
## IBC Packet Types
### DID Query Packet
```rust
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct DIDQueryPacket {
pub query_type: String,
pub did: String,
pub requester: String,
pub callback_id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct DIDResponsePacket {
pub callback_id: String,
pub did_document: Option<DIDDocument>,
pub verification_status: VerificationStatus,
pub error: Option<String>,
}
```
### Cross-Chain Proposal Packet
```rust
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct CrossChainProposalPacket {
pub proposal_type: String,
pub target_module: String, // "did", "dwn", or "svc"
pub action: String,
pub params: Binary,
pub proposer_did: String,
}
```
## Deployment Scripts Update
### Cosmos Hub Deployment
```bash
#!/bin/bash
# deploy_cosmoshub.sh
# Cosmos Hub configuration
CHAIN_ID="cosmoshub-4"
NODE="https://rpc.cosmos.network:443"
DEPLOYER="cosmos1..."
GAS_PRICES="0.025uatom"
# Deploy contracts
gaiad tx wasm store artifacts/identity_dao_core.wasm \
--from $DEPLOYER \
--chain-id $CHAIN_ID \
--node $NODE \
--gas-prices $GAS_PRICES \
--broadcast-mode block
# Instantiate with IBC configuration
INIT_MSG=$(cat <<EOF
{
"admin": "$DEPLOYER",
"dao_name": "Sonr Identity DAO",
"dao_uri": "https://sonr.io/dao",
"ibc_config": {
"sonr_channel": "channel-xxx",
"sonr_port": "did",
"timeout_seconds": 60
}
}
EOF
)
gaiad tx wasm instantiate $CODE_ID "$INIT_MSG" \
--from $DEPLOYER \
--label "identity-dao-core" \
--admin $DEPLOYER
```
### IBC Channel Setup
```bash
#!/bin/bash
# setup_ibc.sh
# Install Hermes relayer
cargo install ibc-relayer-cli --version 1.0.0
# Configure relayer
hermes config init
# Add chains
hermes chains add cosmoshub-4
hermes chains add sonr-1
# Create client, connection, and channel
hermes create channel \
--a-chain cosmoshub-4 \
--b-chain sonr-1 \
--a-port "wasm.$DAO_CONTRACT" \
--b-port "did" \
--order unordered
# Start relayer
hermes start
```
## Testing IBC Integration
### Local Testing with IBC
```go
// tests/e2e/ibc_test.go
func TestIBCDIDQuery(t *testing.T) {
// Setup two chains
cosmosHub := setupCosmosHub()
sonrChain := setupSonrChain()
// Create IBC channel
channel := createIBCChannel(cosmosHub, sonrChain)
// Deploy DAO on Cosmos Hub
daoAddr := deployDAO(cosmosHub)
// Create DID on Sonr
did := createDID(sonrChain, "test-user")
// Query DID via IBC from DAO
response := queryDIDViaIBC(daoAddr, did, channel)
assert.NotNil(t, response)
assert.Equal(t, did, response.Document.ID)
}
```
### Integration Test Script
```bash
#!/bin/bash
# test_ibc_integration.sh
# Start local chains
ignite chain serve -c cosmoshub &
COSMOS_PID=$!
ignite chain serve -c sonr &
SONR_PID=$!
# Wait for chains
sleep 10
# Setup IBC
bash setup_ibc.sh
# Deploy contracts
bash deploy_cosmoshub.sh
# Run test transactions
echo "Testing IBC DID query..."
gaiad tx wasm execute $DAO_ADDR \
'{"query_did_ibc":{"did":"did:sonr:test123"}}' \
--from tester
# Check results
gaiad query wasm contract-state smart $DAO_ADDR \
'{"get_cached_did":{"did":"did:sonr:test123"}}'
# Cleanup
kill $COSMOS_PID $SONR_PID
```
## Security Considerations
### IBC-Specific Security
1. **Channel Authentication**: Verify channel is connected to correct Sonr chain
2. **Packet Validation**: Validate all incoming IBC packets
3. **Timeout Handling**: Handle packet timeouts gracefully
4. **Replay Protection**: Ensure packets cannot be replayed
### Trust Assumptions
1. **Relayer Trust**: Relayers cannot forge packets but can censor
2. **Light Client Security**: Depends on IBC light client security
3. **Cross-Chain Atomicity**: No atomicity guarantees across chains
## Gas Costs
### IBC Operations Gas Estimates
| Operation | Gas Cost |
|-----------|----------|
| IBC Channel Open | 150,000 |
| IBC Packet Send | 80,000 |
| IBC Packet Receive | 100,000 |
| IBC Acknowledgement | 60,000 |
| DID Query via IBC | 180,000 |
## Migration from Stargate to IBC
### Before (Stargate)
```rust
let query = QueryRequest::Stargate {
path: "/sonr.did.Query/GetDIDDocument".to_string(),
data: Binary::from(query_data),
};
let response: DIDDocumentResponse = deps.querier.query(&query)?;
```
### After (IBC)
```rust
let ibc_msg = IbcMsg::SendPacket {
channel_id: channel.id,
data: to_binary(&DIDQueryPacket {
query_type: "get_did_document".to_string(),
did: did.clone(),
requester: info.sender.to_string(),
callback_id: generate_callback_id(),
})?,
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(60)),
};
// Response handled asynchronously in ibc_packet_receive
```
## Monitoring and Maintenance
### Metrics to Track
1. IBC packet success rate
2. Average query latency
3. Channel uptime
4. Relayer performance
### Maintenance Tasks
1. Update relayer configuration
2. Monitor channel health
3. Handle stuck packets
4. Upgrade IBC client
## Conclusion
IBC integration enables the Identity DAO on Cosmos Hub to securely interact with Sonr chain modules. The asynchronous nature of IBC requires architectural changes but provides better security and decentralization compared to direct module access.
-366
View File
@@ -1,366 +0,0 @@
# Identity DAO - Decentralized Identity Governance
A modular Decentralized Identity DAO implementation for the Sonr blockchain, integrating with the x/did module to provide identity-based governance capabilities.
## Overview
The Identity DAO enables decentralized governance where voting power and proposal submission rights are determined by verified identity status rather than token holdings. This creates a more equitable governance system based on identity verification levels.
## Architecture
The DAO follows [DAO DAO's modular architecture](https://github.com/DA0-DA0/dao-contracts) with four core components:
### 1. Identity DAO Core Module
- **Location**: `/contracts/core/`
- **Purpose**: Central coordination and treasury management
- **Key Features**:
- Treasury management for DAO funds
- Proposal execution orchestration
- Module registry and configuration
- Wyoming DAO compliance features
### 2. DID-Based Voting Module
- **Location**: `/contracts/voting/`
- **Purpose**: Identity-based voting power calculation
- **Key Features**:
- Voting power based on DID verification level
- Reputation-weighted voting
- Quorum and threshold management
- Vote tallying and results
### 3. Identity Proposal Module
- **Location**: `/contracts/proposals/`
- **Purpose**: Proposal lifecycle management
- **Key Features**:
- Proposal creation and state management
- Identity-gated proposal types
- Execution scheduling
- Multi-signature support
### 4. Pre-Propose Identity Module
- **Location**: `/contracts/pre-propose/`
- **Purpose**: DID-gated proposal submission
- **Key Features**:
- Verification requirements for proposers
- Deposit management
- Anti-spam mechanisms
- Proposal review workflow
## Installation
### Prerequisites
- Rust 1.70+
- Docker (for contract optimization)
- `snrd` binary (Sonr blockchain node)
- `jq` for JSON processing
### Building Contracts
```bash
# Build all contracts with optimizer
cd contracts/DAO
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/workspace-optimizer:0.13.0
# Artifacts will be in ./artifacts/
```
## Deployment
### Quick Deploy
```bash
# Set environment variables
export CHAIN_ID="sonrtest_1-1"
export NODE="http://localhost:26657"
export DEPLOYER="your-key-name"
# Run deployment script
./scripts/deploy.sh
```
### Manual Deployment
1. **Store Contract Codes**:
```bash
# Store each contract
snrd tx wasm store artifacts/identity_dao_core.wasm \
--from $DEPLOYER --chain-id $CHAIN_ID --gas-prices 0.025usnr
# Repeat for voting, proposals, and pre-propose modules
```
2. **Instantiate Core Module**:
```bash
snrd tx wasm instantiate $CORE_CODE_ID \
'{"admin":"'$DEPLOYER'","dao_name":"Sonr Identity DAO","dao_uri":"https://sonr.io/dao","voting_module":null,"proposal_modules":[]}' \
--from $DEPLOYER --label "identity-dao-core" --admin $DEPLOYER
```
3. **Deploy Other Modules** (see deployment script for details)
4. **Link Modules**:
```bash
# Update core with voting and proposal modules
snrd tx wasm execute $CORE_ADDR \
'{"update_config":{"voting_module":"'$VOTING_ADDR'","proposal_modules":["'$PROPOSALS_ADDR'"]}}' \
--from $DEPLOYER
```
## Contract Interactions
### Creating a Proposal
1. **Submit to Pre-Propose Module**:
```bash
snrd tx wasm execute $PRE_PROPOSE_ADDR \
'{"submit_proposal":{"title":"Upgrade Protocol","description":"Upgrade to v2.0","msgs":[...]}}' \
--from $PROPOSER --amount 1000000usnr
```
2. **Admin Approval** (if required):
```bash
snrd tx wasm execute $PRE_PROPOSE_ADDR \
'{"approve_proposal":{"proposal_id":1}}' \
--from $ADMIN
```
### Voting
```bash
# Cast a vote
snrd tx wasm execute $VOTING_ADDR \
'{"vote":{"proposal_id":1,"vote":"yes"}}' \
--from $VOTER
# Vote options: "yes", "no", "abstain", "no_with_veto"
```
### Executing Proposals
```bash
# After voting period ends and proposal passes
snrd tx wasm execute $PROPOSALS_ADDR \
'{"execute":{"proposal_id":1}}' \
--from $EXECUTOR
```
## Query Commands
### Query Proposal Status
```bash
snrd query wasm contract-state smart $PROPOSALS_ADDR \
'{"proposal":{"proposal_id":1}}'
```
### Query Voting Power
```bash
snrd query wasm contract-state smart $VOTING_ADDR \
'{"voting_power":{"address":"sonr1..."}}'
```
### Query DAO Configuration
```bash
snrd query wasm contract-state smart $CORE_ADDR \
'{"config":{}}'
```
### List Pending Proposals
```bash
snrd query wasm contract-state smart $PRE_PROPOSE_ADDR \
'{"pending_proposals":{}}'
```
## Governance Flow
```mermaid
graph TD
A[DID Holder] -->|Submit Proposal| B[Pre-Propose Module]
B -->|Verify DID Status| C{Verification Check}
C -->|Pass| D[Pending Queue]
C -->|Fail| E[Rejected]
D -->|Admin Review| F{Approval}
F -->|Approved| G[Proposal Module]
F -->|Rejected| H[Refund Deposit]
G -->|Open Voting| I[Voting Module]
I -->|Cast Votes| J[Vote Tally]
J -->|Voting Period Ends| K{Result}
K -->|Pass| L[Execute]
K -->|Fail| M[Archive]
L -->|Run Messages| N[Core Module]
```
## Verification Levels
The DAO recognizes four verification levels:
| Level | Name | Voting Power Multiplier | Proposal Rights |
|-------|------|------------------------|-----------------|
| 0 | Unverified | 0x | None |
| 1 | Basic | 1x | Standard proposals |
| 2 | Advanced | 2x | Policy changes |
| 3 | Full | 3x | All proposals |
## Wyoming DAO Compliance
The Identity DAO includes features for Wyoming DAO legal compliance:
- **Named Entity**: DAO name and URI stored on-chain
- **Member Registry**: DID-based membership tracking
- **Voting Records**: All votes recorded on-chain
- **Treasury Management**: Transparent fund management
- **Governance Rules**: Codified in smart contracts
## Security Considerations
### Access Control
- Admin functions restricted to DAO governance
- Module interactions validated
- DID verification required for participation
### Economic Security
- Deposit requirements for proposals
- Refund mechanisms for failed proposals
- Anti-spam through verification requirements
### Upgrade Security
- Migration support with admin approval
- Code ID tracking for audit trail
- Gradual rollout capabilities
## Testing
### Unit Tests
```bash
cd contracts/DAO
cargo test
```
### Integration Tests
```bash
# Run e2e tests (requires running testnet)
cd tests/e2e
go test -v ./...
```
### Local Testnet
```bash
# Start local testnet
make localnet
# Deploy contracts
./scripts/deploy.sh
# Run test transactions
./scripts/test_governance.sh
```
## Migration
To migrate contracts to new versions:
```bash
# Migrate single module
./scripts/migrate.sh core
# Migrate all modules
./scripts/migrate.sh all
```
## Gas Optimization
### Recommended Gas Settings
- **Store Code**: 5,000,000 gas
- **Instantiate**: 500,000 gas
- **Execute (simple)**: 200,000 gas
- **Execute (complex)**: 1,000,000 gas
- **Query**: No gas required
### Optimization Tips
1. Batch operations when possible
2. Use efficient data structures
3. Minimize storage writes
4. Cache frequently accessed data
## Troubleshooting
### Common Issues
**Issue**: "Insufficient verification level"
- **Solution**: Ensure your DID has the required verification level
**Issue**: "Insufficient deposit"
- **Solution**: Include the minimum deposit amount with proposal submission
**Issue**: "Unauthorized"
- **Solution**: Check that you have the correct role/permissions
**Issue**: "Proposal already executed"
- **Solution**: Proposals can only be executed once
## API Reference
### Core Module
#### Execute Messages
- `UpdateConfig`: Update DAO configuration
- `ExecuteProposal`: Execute approved proposal
- `UpdateAdmin`: Transfer admin rights
#### Query Messages
- `Config`: Get DAO configuration
- `Admin`: Get current admin
- `ProposalModules`: List proposal modules
### Voting Module
#### Execute Messages
- `Vote`: Cast a vote on a proposal
- `UpdateConfig`: Update voting parameters
#### Query Messages
- `VotingPower`: Get address voting power
- `ProposalVotes`: Get votes for a proposal
- `Config`: Get voting configuration
### Proposals Module
#### Execute Messages
- `Propose`: Create a new proposal
- `Execute`: Execute passed proposal
- `Close`: Close expired proposal
#### Query Messages
- `Proposal`: Get proposal details
- `ListProposals`: List all proposals
- `ProposalResult`: Get proposal outcome
### Pre-Propose Module
#### Execute Messages
- `SubmitProposal`: Submit proposal for review
- `ApproveProposal`: Approve pending proposal
- `RejectProposal`: Reject pending proposal
- `WithdrawProposal`: Withdraw pending proposal
#### Query Messages
- `PendingProposals`: List pending proposals
- `DepositInfo`: Get deposit information
- `Config`: Get module configuration
## Contributing
Please see the main [Sonr contribution guidelines](https://sonr.dev/contributing).
## License
This project is licensed under the Apache 2.0 License - see the [LICENSE](../../LICENSE) file for details.
## Support
- GitHub Issues: [sonr-io/sonr](https://github.com/sonr-io/sonr/issues)
- Discord: [Sonr Community](https://discord.gg/sonr)
- Documentation: [docs.sonr.io](https://sonr.dev)
-286
View File
@@ -1,286 +0,0 @@
# Identity DAO Security Audit Report
## Executive Summary
This document provides a comprehensive security analysis of the Identity DAO smart contracts, identifying potential vulnerabilities and recommending mitigations.
## Audit Scope
- **Core Module** (`/contracts/core/`)
- **Voting Module** (`/contracts/voting/`)
- **Proposals Module** (`/contracts/proposals/`)
- **Pre-Propose Module** (`/contracts/pre-propose/`)
## Security Findings
### Critical Issues ✅ (None Found)
No critical vulnerabilities identified that could lead to immediate fund loss or system compromise.
### High Severity Issues
#### H-1: Stargate Query Trust Assumptions
**Location**: All modules using Stargate queries
**Issue**: Contracts assume x/did module responses are always valid
**Impact**: Malicious or compromised x/did module could manipulate governance
**Mitigation**:
```rust
// Add validation for Stargate responses
fn validate_did_response(response: &DIDDocumentResponse) -> Result<(), ContractError> {
// Verify response contains expected fields
if response.document.id.is_empty() {
return Err(ContractError::InvalidDIDResponse {});
}
// Add signature verification if available
Ok(())
}
```
### Medium Severity Issues
#### M-1: Proposal Execution Reentrancy
**Location**: `proposals/src/contract.rs:execute_proposal()`
**Issue**: External calls during proposal execution could re-enter
**Impact**: Potential state manipulation during execution
**Mitigation**:
```rust
// Add reentrancy guard
pub const EXECUTION_LOCK: Item<bool> = Item::new("execution_lock");
fn execute_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result<Response, ContractError> {
// Check and set lock
if EXECUTION_LOCK.may_load(deps.storage)?.unwrap_or(false) {
return Err(ContractError::ReentrantCall {});
}
EXECUTION_LOCK.save(deps.storage, &true)?;
// Execute proposal...
// Clear lock
EXECUTION_LOCK.save(deps.storage, &false)?;
Ok(response)
}
```
#### M-2: Integer Overflow in Voting Power
**Location**: `voting/src/contract.rs:calculate_voting_power()`
**Issue**: Multiplication could overflow for large reputation scores
**Impact**: Incorrect voting power calculation
**Mitigation**:
```rust
// Use checked arithmetic
let power = base_power
.checked_mul(Uint128::from(verification_multiplier))?
.checked_add(Uint128::from(reputation_score))?;
```
### Low Severity Issues
#### L-1: Missing Event Emissions
**Location**: Multiple locations
**Issue**: Some state changes don't emit events
**Impact**: Reduced observability
**Mitigation**: Add comprehensive event emissions for all state changes
#### L-2: Unbounded Iteration
**Location**: `proposals/src/contract.rs:list_proposals()`
**Issue**: Could consume excessive gas with many proposals
**Impact**: DoS potential
**Mitigation**: Enforce pagination limits:
```rust
const MAX_LIMIT: u32 = 100;
let limit = limit.unwrap_or(30).min(MAX_LIMIT);
```
## Access Control Analysis
### Admin Functions
**Properly Protected**:
- Core module admin updates
- Proposal module configuration
- Emergency pause mechanisms
### Public Functions
**Appropriate Restrictions**:
- Voting requires DID verification
- Proposal submission requires minimum verification
- Execution requires proposal to pass
## Gas Optimization Recommendations
### Storage Optimizations
#### 1. Pack Struct Fields
```rust
// Before - 3 storage slots
pub struct ProposalData {
pub id: u64, // 8 bytes
pub status: u8, // 1 byte
pub votes_yes: u128, // 16 bytes - new slot
pub votes_no: u128, // 16 bytes - new slot
}
// After - 2 storage slots
pub struct ProposalData {
pub id: u64, // 8 bytes
pub status: u8, // 1 byte
pub reserved: [u8; 7], // 7 bytes padding
pub votes_yes: u128, // 16 bytes
pub votes_no: u128, // 16 bytes - same slot
}
```
#### 2. Use Lazy Loading
```rust
// Load only needed fields
let proposal_status = PROPOSALS
.may_load(deps.storage, proposal_id)?
.map(|p| p.status)
.ok_or(ContractError::ProposalNotFound {})?;
```
### Computation Optimizations
#### 1. Cache Repeated Calculations
```rust
// Cache voting power instead of recalculating
pub const VOTING_POWER_CACHE: Map<&Addr, (u64, Uint128)> = Map::new("vp_cache");
fn get_voting_power(deps: Deps, address: &Addr, height: u64) -> StdResult<Uint128> {
// Check cache first
if let Some((cached_height, power)) = VOTING_POWER_CACHE.may_load(deps.storage, address)? {
if cached_height == height {
return Ok(power);
}
}
// Calculate and cache if not found
let power = calculate_voting_power(deps, address)?;
VOTING_POWER_CACHE.save(deps.storage, address, &(height, power))?;
Ok(power)
}
```
#### 2. Batch Operations
```rust
// Process multiple votes in one transaction
pub fn execute_batch_vote(
deps: DepsMut,
info: MessageInfo,
votes: Vec<(u64, Vote)>,
) -> Result<Response, ContractError> {
let mut response = Response::new();
for (proposal_id, vote) in votes {
// Process each vote
response = response.add_attribute("vote", format!("{}:{:?}", proposal_id, vote));
}
Ok(response)
}
```
## Best Practices Compliance
### ✅ Implemented
- Proper error handling with custom error types
- Input validation on all entry points
- State isolation between modules
- Upgrade mechanisms with admin control
### ⚠️ Recommendations
1. Add circuit breaker for emergency pause
2. Implement time locks for critical operations
3. Add slashing mechanisms for malicious proposals
4. Include rate limiting for proposal submissions
## Testing Recommendations
### Unit Tests Required
```rust
#[cfg(test)]
mod security_tests {
#[test]
fn test_reentrancy_protection() {
// Test reentrancy guard
}
#[test]
fn test_overflow_protection() {
// Test arithmetic overflow handling
}
#[test]
fn test_access_control() {
// Test unauthorized access attempts
}
}
```
### Fuzzing Targets
1. Voting power calculation with extreme values
2. Proposal execution with complex message sequences
3. Deposit/refund mechanics with edge cases
## Formal Verification Recommendations
### Properties to Verify
1. **Conservation of Votes**: Total votes never exceed total voting power
2. **Proposal Finality**: Executed proposals cannot be re-executed
3. **Deposit Safety**: Deposits are always refundable or consumed
### Invariants
```rust
// Invariant: Sum of all votes <= Total voting power
assert!(total_yes + total_no + total_abstain <= total_voting_power);
// Invariant: Proposal status transitions are one-way
assert!(!(status == Status::Executed && new_status == Status::Open));
```
## Security Checklist
### Pre-Deployment
- [ ] Run static analysis tools (cargo-audit, clippy)
- [ ] Complete test coverage > 90%
- [ ] Perform gas profiling
- [ ] External security audit
- [ ] Bug bounty program setup
### Post-Deployment
- [ ] Monitor for unusual activity
- [ ] Regular security updates
- [ ] Incident response plan
- [ ] Upgrade procedures tested
## Conclusion
The Identity DAO contracts demonstrate solid security practices with no critical vulnerabilities identified. The recommended improvements focus on:
1. **Enhanced validation** of external data sources
2. **Gas optimizations** for scalability
3. **Additional safety mechanisms** for edge cases
Implementation priority:
1. High severity mitigations (H-1)
2. Gas optimizations for frequently called functions
3. Medium severity fixes (M-1, M-2)
4. Low severity improvements
## Appendix: Security Tools
### Recommended Tools
- **Static Analysis**: `cargo-audit`, `clippy`
- **Fuzzing**: `cargo-fuzz`, `honggfuzz-rs`
- **Formal Verification**: `KEVM`, `Certora`
- **Gas Profiling**: `cosmwasm-gas-reporter`
### Audit Commands
```bash
# Security checks
cargo audit
cargo clippy -- -W clippy::all
# Test coverage
cargo tarpaulin --out Html
# Gas profiling
cargo test --features gas_profiling
```

Some files were not shown because too many files have changed in this diff Show More