2025-10-03 14:45:52 -04:00
|
|
|
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 {
|
2025-10-10 11:47:18 -04:00
|
|
|
// 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)
|
2025-10-03 14:45:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|