mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-03 01:41:44 +00:00
* 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>
727 lines
24 KiB
Go
727 lines
24 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"cosmossdk.io/log"
|
|
"github.com/cosmos/cosmos-sdk/client"
|
|
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
|
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
|
"github.com/sonr-io/sonr/x/did/client/server"
|
|
"github.com/sonr-io/sonr/x/did/types"
|
|
)
|
|
|
|
// RegisterUserWithWebAuthn registers a new user using WebAuthn through browser interaction
|
|
func RegisterUserWithWebAuthn(username string) error {
|
|
logger := log.NewLogger(os.Stderr)
|
|
|
|
// If no username provided, prompt for it using standard input
|
|
if strings.TrimSpace(username) == "" {
|
|
var err error
|
|
username, err = promptForUsername()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get username: %w", err)
|
|
}
|
|
}
|
|
|
|
// Initialize database and check if username already exists
|
|
if err := server.InitDB(); err != nil {
|
|
logger.Warn("Failed to initialize database", "error", err)
|
|
// Continue without username check - database may not be available
|
|
} else {
|
|
// Check if username already exists
|
|
service := server.NewWebAuthnCredentialService()
|
|
existingCredentials, err := service.GetByUsername(username)
|
|
if err == nil && len(existingCredentials) > 0 {
|
|
return fmt.Errorf("username '%s' already exists with %d WebAuthn credential(s)", username, len(existingCredentials))
|
|
}
|
|
// If error occurred (like record not found), continue with registration
|
|
}
|
|
|
|
// Find available port for auth server
|
|
port, err := findAvailablePort()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find available port: %w", err)
|
|
}
|
|
|
|
// Create channel to signal completion
|
|
done := make(chan error, 1)
|
|
|
|
// Setup server with WebAuthn registration context
|
|
err = server.StartAuthServerWithWebAuthn(port, username, done)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start auth server: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if stopErr := server.StopAuthServer(); stopErr != nil {
|
|
logger.Error("Failed to stop auth server", "error", stopErr)
|
|
}
|
|
}()
|
|
|
|
// Wait for server to be ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Open browser to WebAuthn registration page
|
|
url := fmt.Sprintf("http://localhost:%d/register?username=%s", port, username)
|
|
logger.Info("Opening browser for WebAuthn registration", "url", url)
|
|
|
|
if err := openBrowser(url); err != nil {
|
|
logger.Warn("Failed to open browser automatically", "error", err)
|
|
logger.Info("Please navigate manually to the URL", "url", url)
|
|
}
|
|
|
|
logger.Info("Waiting for WebAuthn registration to complete...")
|
|
|
|
// Wait for registration to complete or timeout
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
|
}
|
|
logger.Info("WebAuthn registration completed successfully")
|
|
return nil
|
|
case <-time.After(30 * time.Second):
|
|
logger.Warn("WebAuthn registration timed out after 30 seconds")
|
|
return fmt.Errorf("WebAuthn registration timed out after 30 seconds - please try again")
|
|
}
|
|
}
|
|
|
|
// findAvailablePort finds an available port starting from 8080
|
|
func findAvailablePort() (int, error) {
|
|
for port := 8080; port < 8090; port++ {
|
|
conn, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
|
if err == nil {
|
|
conn.Close()
|
|
return port, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("no available port found in range 8080-8090")
|
|
}
|
|
|
|
// openBrowser opens the default browser with the given URL
|
|
func openBrowser(url string) error {
|
|
var cmd string
|
|
var args []string
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
cmd = "open"
|
|
args = []string{url}
|
|
case "linux":
|
|
cmd = "xdg-open"
|
|
args = []string{url}
|
|
case "windows":
|
|
cmd = "rundll32"
|
|
args = []string{"url.dll,FileProtocolHandler", url}
|
|
default:
|
|
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
|
}
|
|
|
|
return exec.Command(cmd, args...).Start()
|
|
}
|
|
|
|
// RegisterUserWithWebAuthnAndBroadcast registers a user with WebAuthn and broadcasts to blockchain
|
|
func RegisterUserWithWebAuthnAndBroadcast(
|
|
clientCtx client.Context,
|
|
username string,
|
|
autoCreateVault bool,
|
|
) error {
|
|
// Import necessary packages
|
|
var (
|
|
contextPkg = "context"
|
|
base64Pkg = "encoding/base64"
|
|
jsonPkg = "encoding/json"
|
|
flagsPkg = "github.com/cosmos/cosmos-sdk/client/flags"
|
|
txPkg = "github.com/cosmos/cosmos-sdk/client/tx"
|
|
sdkPkg = "github.com/cosmos/cosmos-sdk/types"
|
|
typesPkg = "github.com/sonr-io/sonr/x/did/types"
|
|
)
|
|
_ = contextPkg
|
|
_ = base64Pkg
|
|
_ = jsonPkg
|
|
_ = flagsPkg
|
|
_ = txPkg
|
|
_ = sdkPkg
|
|
_ = typesPkg
|
|
|
|
logger := log.NewLogger(os.Stderr)
|
|
|
|
// If no username provided, prompt for it using standard input
|
|
if strings.TrimSpace(username) == "" {
|
|
var err error
|
|
username, err = promptForUsername()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get username: %w", err)
|
|
}
|
|
}
|
|
|
|
// Initialize database and check if username already exists
|
|
if err := server.InitDB(); err != nil {
|
|
logger.Warn("Failed to initialize database", "error", err)
|
|
// Continue without username check - database may not be available
|
|
} else {
|
|
// Check if username already exists
|
|
service := server.NewWebAuthnCredentialService()
|
|
existingCredentials, err := service.GetByUsername(username)
|
|
if err == nil && len(existingCredentials) > 0 {
|
|
return fmt.Errorf("username '%s' already exists with %d WebAuthn credential(s)", username, len(existingCredentials))
|
|
}
|
|
// If error occurred (like record not found), continue with registration
|
|
}
|
|
|
|
// Find available port for auth server
|
|
port, err := findAvailablePort()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find available port: %w", err)
|
|
}
|
|
|
|
// Create channel to signal completion and pass WebAuthn credential data
|
|
done := make(chan error, 1)
|
|
credentialData := make(chan *server.WebAuthnCredential, 1)
|
|
|
|
// Setup server with WebAuthn registration context and credential data channel
|
|
err = server.StartAuthServerWithWebAuthnAndCredentialChannel(
|
|
port,
|
|
username,
|
|
done,
|
|
credentialData,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start auth server: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if stopErr := server.StopAuthServer(); stopErr != nil {
|
|
logger.Error("Failed to stop auth server", "error", stopErr)
|
|
}
|
|
}()
|
|
|
|
// Wait for server to be ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Open browser to WebAuthn registration page
|
|
url := fmt.Sprintf("http://localhost:%d/register?username=%s", port, username)
|
|
logger.Info("Opening browser for WebAuthn registration", "url", url)
|
|
|
|
if err := openBrowser(url); err != nil {
|
|
logger.Warn("Failed to open browser automatically", "error", err)
|
|
logger.Info("Please navigate manually to the URL", "url", url)
|
|
}
|
|
|
|
logger.Info("Waiting for WebAuthn registration to complete...")
|
|
|
|
// Wait for registration to complete or timeout
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
|
}
|
|
logger.Info("WebAuthn registration completed successfully")
|
|
|
|
// Get the credential data from the server
|
|
select {
|
|
case credential := <-credentialData:
|
|
logger.Info("Received WebAuthn credential data, broadcasting to blockchain...",
|
|
"credentialID", credential.CredentialID, "username", credential.Username)
|
|
|
|
// Create and broadcast the MsgRegisterWebAuthnCredential transaction
|
|
err = broadcastWebAuthnCredential(clientCtx, credential, autoCreateVault)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to broadcast WebAuthn credential: %w", err)
|
|
}
|
|
|
|
logger.Info(
|
|
"WebAuthn credential successfully broadcast to blockchain and vault creation initiated",
|
|
)
|
|
return nil
|
|
case <-time.After(2 * time.Second):
|
|
return fmt.Errorf("failed to receive credential data from server")
|
|
}
|
|
case <-time.After(30 * time.Second):
|
|
logger.Warn("WebAuthn registration timed out after 30 seconds")
|
|
return fmt.Errorf("WebAuthn registration timed out after 30 seconds - please try again")
|
|
}
|
|
}
|
|
|
|
// RegisterUserWithWebAuthnAndBroadcastWithAssertion registers a user with WebAuthn and assertion methods
|
|
func RegisterUserWithWebAuthnAndBroadcastWithAssertion(
|
|
clientCtx client.Context,
|
|
username string, // Can be empty, will use assertion value
|
|
autoCreateVault bool,
|
|
assertionType string,
|
|
assertionValue string,
|
|
) error {
|
|
// Import necessary packages
|
|
var (
|
|
contextPkg = "context"
|
|
base64Pkg = "encoding/base64"
|
|
jsonPkg = "encoding/json"
|
|
flagsPkg = "github.com/cosmos/cosmos-sdk/client/flags"
|
|
txPkg = "github.com/cosmos/cosmos-sdk/client/tx"
|
|
sdkPkg = "github.com/cosmos/cosmos-sdk/types"
|
|
typesPkg = "github.com/sonr-io/sonr/x/did/types"
|
|
)
|
|
_ = contextPkg
|
|
_ = base64Pkg
|
|
_ = jsonPkg
|
|
_ = flagsPkg
|
|
_ = txPkg
|
|
_ = sdkPkg
|
|
_ = typesPkg
|
|
|
|
logger := log.NewLogger(os.Stderr)
|
|
|
|
// Use assertion value as the identifier
|
|
identifier := assertionValue
|
|
|
|
// Initialize database and check if assertion already exists
|
|
if err := server.InitDB(); err != nil {
|
|
logger.Warn("Failed to initialize database", "error", err)
|
|
// Continue without check - database may not be available
|
|
} else {
|
|
// Check if assertion value already exists as a registered identity
|
|
service := server.NewWebAuthnCredentialService()
|
|
existingCredentials, err := service.GetByUsername(identifier)
|
|
if err == nil && len(existingCredentials) > 0 {
|
|
return fmt.Errorf("%s '%s' already registered with %d WebAuthn credential(s)",
|
|
assertionType, assertionValue, len(existingCredentials))
|
|
}
|
|
// If error occurred (like record not found), continue with registration
|
|
}
|
|
|
|
// Find available port for auth server
|
|
port, err := findAvailablePort()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find available port: %w", err)
|
|
}
|
|
|
|
// Create channel to signal completion and pass WebAuthn credential data
|
|
done := make(chan error, 1)
|
|
credentialData := make(chan *server.WebAuthnCredential, 1)
|
|
|
|
// Setup server with WebAuthn registration context and credential data channel
|
|
// Use the assertion value as the identifier for WebAuthn
|
|
err = server.StartAuthServerWithWebAuthnAndCredentialChannel(
|
|
port,
|
|
identifier,
|
|
done,
|
|
credentialData,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start auth server: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if stopErr := server.StopAuthServer(); stopErr != nil {
|
|
logger.Error("Failed to stop auth server", "error", stopErr)
|
|
}
|
|
}()
|
|
|
|
// Wait for server to be ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Open browser to WebAuthn registration page
|
|
url := fmt.Sprintf("http://localhost:%d/register?identifier=%s", port, identifier)
|
|
logger.Info("Opening browser for WebAuthn registration", "url", url)
|
|
|
|
if err := openBrowser(url); err != nil {
|
|
logger.Warn("Failed to open browser automatically", "error", err)
|
|
logger.Info("Please navigate manually to the URL", "url", url)
|
|
}
|
|
|
|
logger.Info("Waiting for WebAuthn registration to complete...")
|
|
|
|
// Wait for registration to complete or timeout
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
|
}
|
|
logger.Info("WebAuthn registration completed successfully")
|
|
|
|
// Get the credential data from the server
|
|
select {
|
|
case credential := <-credentialData:
|
|
logger.Info("Received WebAuthn credential data, broadcasting to blockchain...",
|
|
"credentialID", credential.CredentialID,
|
|
"identifier", identifier,
|
|
"assertionType", assertionType,
|
|
"assertionValue", assertionValue)
|
|
|
|
// Create and broadcast the MsgRegisterWebAuthnCredential transaction with assertion
|
|
err = broadcastWebAuthnCredentialWithAssertion(
|
|
clientCtx, credential, autoCreateVault, assertionType, assertionValue,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to broadcast WebAuthn credential: %w", err)
|
|
}
|
|
|
|
logger.Info(
|
|
"WebAuthn credential successfully broadcast to blockchain with assertion method",
|
|
"assertionType", assertionType,
|
|
)
|
|
return nil
|
|
case <-time.After(2 * time.Second):
|
|
return fmt.Errorf("failed to receive credential data from server")
|
|
}
|
|
case <-time.After(30 * time.Second):
|
|
logger.Warn("WebAuthn registration timed out after 30 seconds")
|
|
return fmt.Errorf("WebAuthn registration timed out after 30 seconds - please try again")
|
|
}
|
|
}
|
|
|
|
// broadcastWebAuthnCredential creates and broadcasts a MsgRegisterWebAuthnCredential transaction
|
|
func broadcastWebAuthnCredential(
|
|
clientCtx client.Context,
|
|
credential *server.WebAuthnCredential,
|
|
autoCreateVault bool,
|
|
) error {
|
|
logger := log.NewLogger(os.Stderr)
|
|
logger.Info("Broadcasting WebAuthn credential transaction",
|
|
"credentialID", credential.CredentialID,
|
|
"username", credential.Username,
|
|
"autoCreateVault", autoCreateVault,
|
|
"chainID", clientCtx.ChainID)
|
|
|
|
// Import required packages
|
|
didtypes := "github.com/sonr-io/sonr/x/did/types"
|
|
_ = didtypes
|
|
|
|
// For gasless transactions, we generate a deterministic address from the WebAuthn credential
|
|
// This allows the transaction to be processed without a pre-existing account
|
|
controllerAddr := generateAddressFromWebAuthn(credential)
|
|
|
|
// Create the WebAuthn credential message
|
|
// PublicKey, Algorithm, and Origin are extracted server-side from attestation
|
|
webauthnCred := types.WebAuthnCredential{
|
|
CredentialId: credential.CredentialID,
|
|
RawId: credential.RawID,
|
|
ClientDataJson: credential.ClientDataJSON,
|
|
AttestationObject: credential.AttestationObject,
|
|
// Use the extracted fields from server processing
|
|
PublicKey: credential.PublicKey,
|
|
Algorithm: credential.Algorithm,
|
|
Origin: credential.Origin,
|
|
}
|
|
|
|
// Create the registration message
|
|
msg := &types.MsgRegisterWebAuthnCredential{
|
|
Controller: controllerAddr.String(),
|
|
Username: credential.Username,
|
|
WebauthnCredential: webauthnCred,
|
|
VerificationMethodId: fmt.Sprintf("webauthn-%s", credential.CredentialID[:8]),
|
|
AutoCreateVault: autoCreateVault,
|
|
}
|
|
|
|
// Build the transaction with proper signature structure for gasless handling
|
|
txBuilder := clientCtx.TxConfig.NewTxBuilder()
|
|
err := txBuilder.SetMsgs(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set message: %w", err)
|
|
}
|
|
|
|
// Set reasonable gas limit for gasless transaction (fees will still be zero)
|
|
txBuilder.SetGasLimit(200000) // Reasonable gas limit for WebAuthn registration
|
|
txBuilder.SetFeeAmount(sdk.NewCoins()) // Zero fees - gasless
|
|
|
|
// For WebAuthn gasless transactions, we need to provide at least empty signature info
|
|
// to pass mempool validation, then our ante handler will bypass signature verification
|
|
logger.Info("Creating gasless WebAuthn transaction with empty signature placeholder",
|
|
"controllerAddress", controllerAddr.String(),
|
|
"credentialID", credential.CredentialID)
|
|
|
|
// For WebAuthn gasless transactions, we need to provide a dummy signature to pass
|
|
// mempool validation, then our ante handler will bypass the verification
|
|
logger.Info("Creating dummy signature for mempool validation bypass")
|
|
|
|
// Create a minimal dummy public key from the controller address
|
|
// This is needed so the signature validation doesn't fail immediately
|
|
pubKeyBytes := make(
|
|
[]byte,
|
|
33,
|
|
) // Standard secp256k1 compressed public key length
|
|
copy(pubKeyBytes[1:], controllerAddr.Bytes()[:32]) // Use controller address bytes
|
|
pubKeyBytes[0] = 0x02 // Compressed public key prefix
|
|
|
|
dummyPubKey := &secp256k1.PubKey{Key: pubKeyBytes}
|
|
|
|
// Create a minimal dummy signature structure to pass mempool validation
|
|
dummySig := signing.SignatureV2{
|
|
PubKey: dummyPubKey, // Dummy public key derived from controller address
|
|
Data: &signing.SingleSignatureData{
|
|
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
|
|
Signature: make([]byte, 64), // Non-empty signature to pass basic checks
|
|
},
|
|
Sequence: 0, // Zero sequence for gasless
|
|
}
|
|
|
|
// Set the dummy signature to pass mempool validation
|
|
err = txBuilder.SetSignatures(dummySig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set dummy signature: %w", err)
|
|
}
|
|
|
|
logger.Info(
|
|
"Dummy signature set for mempool bypass",
|
|
"pubKeyLen",
|
|
len(pubKeyBytes),
|
|
"sigLen",
|
|
64,
|
|
)
|
|
|
|
// Encode the transaction
|
|
tx := txBuilder.GetTx()
|
|
|
|
// Debug: Verify transaction has no signatures (expected for WebAuthn bypass)
|
|
if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok {
|
|
sigs, err := sigTx.GetSignaturesV2()
|
|
if err != nil {
|
|
logger.Error("Failed to get signatures from tx", "error", err)
|
|
} else {
|
|
logger.Info("Transaction signature count", "sigCount", len(sigs))
|
|
}
|
|
}
|
|
|
|
txBytes, err := clientCtx.TxConfig.TxEncoder()(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to encode transaction: %w", err)
|
|
}
|
|
|
|
// Broadcast the transaction
|
|
res, err := clientCtx.BroadcastTxSync(txBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to broadcast transaction: %w", err)
|
|
}
|
|
|
|
// Check the response
|
|
if res.Code != 0 {
|
|
return fmt.Errorf("transaction failed with code %d: %s", res.Code, res.RawLog)
|
|
}
|
|
|
|
logger.Info("WebAuthn credential successfully registered",
|
|
"txHash", res.TxHash,
|
|
"height", res.Height,
|
|
"gasUsed", res.GasUsed)
|
|
|
|
// Parse the response to get the created DID
|
|
// In a real implementation, we would parse the events to extract the DID
|
|
logger.Info("DID created successfully",
|
|
"username", credential.Username,
|
|
"credentialID", credential.CredentialID,
|
|
"vaultCreated", autoCreateVault)
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateAddressFromWebAuthn generates a deterministic address from WebAuthn credential
|
|
func generateAddressFromWebAuthn(credential *server.WebAuthnCredential) sdk.AccAddress {
|
|
// Use the local types package for address generation
|
|
// It returns a hex string with 0x prefix
|
|
addrHex := types.GenerateAddressFromCredential(credential.CredentialID)
|
|
// Remove 0x prefix
|
|
addrHex = strings.TrimPrefix(addrHex, "0x")
|
|
// Decode hex to bytes
|
|
addrBytes := make([]byte, 20)
|
|
for i := 0; i < 20; i++ {
|
|
fmt.Sscanf(addrHex[i*2:i*2+2], "%02x", &addrBytes[i])
|
|
}
|
|
return sdk.AccAddress(addrBytes)
|
|
}
|
|
|
|
// promptForUsername prompts the user for a username using standard input
|
|
func promptForUsername() (string, error) {
|
|
fmt.Print("Enter username for WebAuthn registration: ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
username, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read username input: %w", err)
|
|
}
|
|
|
|
username = strings.TrimSpace(username)
|
|
|
|
// Validate username
|
|
if username == "" {
|
|
return "", fmt.Errorf("username is required")
|
|
}
|
|
if len(username) < 3 {
|
|
return "", fmt.Errorf("username must be at least 3 characters")
|
|
}
|
|
if len(username) > 20 {
|
|
return "", fmt.Errorf("username cannot exceed 20 characters")
|
|
}
|
|
// Check for valid characters (alphanumeric and underscore)
|
|
for _, char := range username {
|
|
if (char < 'a' || char > 'z') &&
|
|
(char < 'A' || char > 'Z') &&
|
|
(char < '0' || char > '9') &&
|
|
char != '_' {
|
|
return "", fmt.Errorf(
|
|
"username can only contain alphanumeric characters and underscores",
|
|
)
|
|
}
|
|
}
|
|
|
|
return username, nil
|
|
}
|
|
|
|
// broadcastWebAuthnCredentialWithAssertion creates and broadcasts a MsgRegisterWebAuthnCredential transaction with assertion
|
|
func broadcastWebAuthnCredentialWithAssertion(
|
|
clientCtx client.Context,
|
|
credential *server.WebAuthnCredential,
|
|
autoCreateVault bool,
|
|
assertionType string,
|
|
assertionValue string,
|
|
) error {
|
|
logger := log.NewLogger(os.Stderr)
|
|
logger.Info("Broadcasting WebAuthn credential transaction with assertion",
|
|
"credentialID", credential.CredentialID,
|
|
"username", credential.Username,
|
|
"autoCreateVault", autoCreateVault,
|
|
"assertionType", assertionType,
|
|
"assertionValue", assertionValue,
|
|
"chainID", clientCtx.ChainID)
|
|
|
|
// Import required packages
|
|
didtypes := "github.com/sonr-io/sonr/x/did/types"
|
|
_ = didtypes
|
|
|
|
// For gasless transactions, we generate a deterministic address from the WebAuthn credential
|
|
// This allows the transaction to be processed without a pre-existing account
|
|
controllerAddr := generateAddressFromWebAuthn(credential)
|
|
|
|
// Create the WebAuthn credential message
|
|
// PublicKey, Algorithm, and Origin are extracted server-side from attestation
|
|
webauthnCred := types.WebAuthnCredential{
|
|
CredentialId: credential.CredentialID,
|
|
RawId: credential.RawID,
|
|
ClientDataJson: credential.ClientDataJSON,
|
|
AttestationObject: credential.AttestationObject,
|
|
// Use the extracted fields from server processing
|
|
PublicKey: credential.PublicKey,
|
|
Algorithm: credential.Algorithm,
|
|
Origin: credential.Origin,
|
|
}
|
|
|
|
// Create the registration message
|
|
// Use the assertion value directly as the username for the message
|
|
// The server will detect the type (email/tel) based on the format
|
|
msg := &types.MsgRegisterWebAuthnCredential{
|
|
Controller: controllerAddr.String(),
|
|
Username: assertionValue, // This will be the email or phone number
|
|
WebauthnCredential: webauthnCred,
|
|
VerificationMethodId: fmt.Sprintf("webauthn-%s", credential.CredentialID[:8]),
|
|
AutoCreateVault: autoCreateVault,
|
|
}
|
|
|
|
// Build the transaction with proper signature structure for gasless handling
|
|
txBuilder := clientCtx.TxConfig.NewTxBuilder()
|
|
err := txBuilder.SetMsgs(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set message: %w", err)
|
|
}
|
|
|
|
// Set reasonable gas limit for gasless transaction (fees will still be zero)
|
|
txBuilder.SetGasLimit(200000) // Reasonable gas limit for WebAuthn registration
|
|
txBuilder.SetFeeAmount(sdk.NewCoins()) // Zero fees - gasless
|
|
|
|
// For WebAuthn gasless transactions, we need to provide at least empty signature info
|
|
// to pass mempool validation, then our ante handler will bypass signature verification
|
|
logger.Info("Creating gasless WebAuthn transaction with empty signature placeholder",
|
|
"controllerAddress", controllerAddr.String(),
|
|
"credentialID", credential.CredentialID)
|
|
|
|
// For WebAuthn gasless transactions, we need to provide a dummy signature to pass
|
|
// mempool validation, then our ante handler will bypass the verification
|
|
logger.Info("Creating dummy signature for mempool validation bypass")
|
|
|
|
// Create a minimal dummy public key from the controller address
|
|
// This is needed so the signature validation doesn't fail immediately
|
|
pubKeyBytes := make(
|
|
[]byte,
|
|
33,
|
|
) // Standard secp256k1 compressed public key length
|
|
copy(pubKeyBytes[1:], controllerAddr.Bytes()[:32]) // Use controller address bytes
|
|
pubKeyBytes[0] = 0x02 // Compressed public key prefix
|
|
|
|
dummyPubKey := &secp256k1.PubKey{Key: pubKeyBytes}
|
|
|
|
// Create a minimal dummy signature structure to pass mempool validation
|
|
dummySig := signing.SignatureV2{
|
|
PubKey: dummyPubKey, // Dummy public key derived from controller address
|
|
Data: &signing.SingleSignatureData{
|
|
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
|
|
Signature: make([]byte, 64), // Non-empty signature to pass basic checks
|
|
},
|
|
Sequence: 0, // Zero sequence for gasless
|
|
}
|
|
|
|
// Set the dummy signature to pass mempool validation
|
|
err = txBuilder.SetSignatures(dummySig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set dummy signature: %w", err)
|
|
}
|
|
|
|
logger.Info(
|
|
"Dummy signature set for mempool bypass",
|
|
"pubKeyLen",
|
|
len(pubKeyBytes),
|
|
"sigLen",
|
|
64,
|
|
)
|
|
|
|
// Encode the transaction
|
|
tx := txBuilder.GetTx()
|
|
|
|
// Debug: Verify transaction has no signatures (expected for WebAuthn bypass)
|
|
if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok {
|
|
sigs, err := sigTx.GetSignaturesV2()
|
|
if err != nil {
|
|
logger.Error("Failed to get signatures from tx", "error", err)
|
|
} else {
|
|
logger.Info("Transaction signature count", "sigCount", len(sigs))
|
|
}
|
|
}
|
|
|
|
txBytes, err := clientCtx.TxConfig.TxEncoder()(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to encode transaction: %w", err)
|
|
}
|
|
|
|
// Broadcast the transaction
|
|
res, err := clientCtx.BroadcastTxSync(txBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to broadcast transaction: %w", err)
|
|
}
|
|
|
|
// Check the response
|
|
if res.Code != 0 {
|
|
return fmt.Errorf("transaction failed with code %d: %s", res.Code, res.RawLog)
|
|
}
|
|
|
|
logger.Info("WebAuthn credential with assertion successfully registered",
|
|
"txHash", res.TxHash,
|
|
"height", res.Height,
|
|
"gasUsed", res.GasUsed,
|
|
"assertionType", assertionType)
|
|
|
|
// Parse the response to get the created DID
|
|
// In a real implementation, we would parse the events to extract the DID
|
|
logger.Info("DID created successfully with assertion method",
|
|
"credentialID", credential.CredentialID,
|
|
"assertionType", assertionType,
|
|
"assertionValue", assertionValue,
|
|
"vaultCreated", autoCreateVault)
|
|
|
|
return nil
|
|
}
|