Files
sonr/x/did/client/cli/register.go
T
40eadc995e 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>
2025-10-10 11:47:18 -04:00

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
}