mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 09:21:39 +00:00
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:
+3
-38
@@ -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
@@ -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
@@ -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:"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
.dockerignore
|
||||
.goreleaser.yml
|
||||
api
|
||||
contracts
|
||||
chains
|
||||
crypto
|
||||
proto
|
||||
test
|
||||
**/_test.go
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() == "*"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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(®istration); 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)
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(®Response); 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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\\)(!)?:"
|
||||
@@ -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:'
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package main
|
||||
|
||||
// Version is set by commitizen during release process
|
||||
var Version = "dev"
|
||||
@@ -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\\)(!)?:"
|
||||
@@ -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:"
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package main
|
||||
|
||||
// Version is set by commitizen during release process
|
||||
var Version = "dev"
|
||||
@@ -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:"
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
|
||||
)
|
||||
@@ -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\\)(!)?:"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package main
|
||||
|
||||
// Version is set by commitizen during release process
|
||||
var Version = "dev"
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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
Reference in New Issue
Block a user