Files
sonr/x/did/client/cli/register.go
T

727 lines
24 KiB
Go
Raw Normal View History

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
}