* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+363
View File
@@ -0,0 +1,363 @@
// Package auth provides gasless transaction support for WebAuthn operations.
package auth
import (
"context"
"encoding/base64"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/tx"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// GaslessTransactionManager handles gasless transactions for WebAuthn.
type GaslessTransactionManager interface {
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
CreateGaslessRegistration(ctx context.Context, credential *WebAuthnCredential, opts *GaslessRegistrationOptions) (*GaslessTransaction, error)
// BroadcastGasless broadcasts a gasless transaction.
BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error)
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
IsEligibleForGasless(msgs []sdk.Msg) bool
// EstimateGaslessGas estimates gas for a gasless transaction.
EstimateGaslessGas(msgType string) uint64
}
// GaslessRegistrationOptions configures gasless WebAuthn registration.
type GaslessRegistrationOptions struct {
Username string `json:"username"`
AutoCreateVault bool `json:"auto_create_vault"`
WebAuthnChallenge []byte `json:"webauthn_challenge"`
DIDDocument map[string]any `json:"did_document,omitempty"`
}
// GaslessTransaction represents a gasless transaction.
type GaslessTransaction struct {
Messages []sdk.Msg `json:"messages"`
Memo string `json:"memo"`
GasLimit uint64 `json:"gas_limit"`
SignerAddress string `json:"signer_address"`
SignMode signing.SignMode `json:"sign_mode"`
TxBytes []byte `json:"tx_bytes,omitempty"`
}
// BroadcastResult contains the result of broadcasting a transaction.
type BroadcastResult struct {
TxHash string `json:"tx_hash"`
Height int64 `json:"height"`
Code uint32 `json:"code"`
RawLog string `json:"raw_log"`
GasUsed int64 `json:"gas_used"`
GasWanted int64 `json:"gas_wanted"`
}
// gaslessManager implements GaslessTransactionManager.
type gaslessManager struct {
txBuilder tx.TxBuilder
broadcaster tx.Broadcaster
config *config.NetworkConfig
}
// NewGaslessTransactionManager creates a new gasless transaction manager.
func NewGaslessTransactionManager(
txBuilder tx.TxBuilder,
broadcaster tx.Broadcaster,
cfg *config.NetworkConfig,
) GaslessTransactionManager {
return &gaslessManager{
txBuilder: txBuilder,
broadcaster: broadcaster,
config: cfg,
}
}
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
func (gm *gaslessManager) CreateGaslessRegistration(
ctx context.Context,
credential *WebAuthnCredential,
opts *GaslessRegistrationOptions,
) (*GaslessTransaction, error) {
// Generate deterministic address from WebAuthn credential
signerAddr := gm.generateAddressFromWebAuthn(credential)
// Convert credential ID to base64 string
credentialID := base64.RawURLEncoding.EncodeToString(credential.RawID)
// Create WebAuthn credential for the message
webauthnCred := didtypes.WebAuthnCredential{
CredentialId: credentialID,
PublicKey: credential.PublicKey,
AttestationType: credential.AttestationType,
Origin: "http://localhost", // Default origin
Algorithm: -7, // ES256 algorithm
CreatedAt: time.Now().Unix(),
RpId: "localhost",
RpName: "Sonr Local",
Transports: credential.Transports,
UserVerified: false, // Default to false for gasless
}
// Set user verification if flags are available
if credential.Flags != nil {
webauthnCred.UserVerified = credential.Flags.UserVerified
}
// Create the registration message
msg := &didtypes.MsgRegisterWebAuthnCredential{
Controller: signerAddr.String(),
Username: opts.Username,
WebauthnCredential: webauthnCred,
AutoCreateVault: opts.AutoCreateVault,
}
// Create gasless transaction
gaslessTx := &GaslessTransaction{
Messages: []sdk.Msg{msg},
Memo: "WebAuthn Gasless Registration",
GasLimit: 200000, // Fixed gas limit for WebAuthn registration
SignerAddress: signerAddr.String(),
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
}
// Build the transaction using fluent interface
gm.txBuilder = gm.txBuilder.
ClearMessages().
AddMessage(msg).
WithMemo(gaslessTx.Memo).
WithGasLimit(gaslessTx.GasLimit).
WithFee(sdk.NewCoins()) // Zero fees for gasless
// Build unsigned transaction
unsignedTx, err := gm.txBuilder.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrInvalidTransaction, "failed to build gasless transaction")
}
// Store transaction bytes
gaslessTx.TxBytes = unsignedTx.SignBytes
return gaslessTx, nil
}
// BroadcastGasless broadcasts a gasless transaction.
func (gm *gaslessManager) BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error) {
// Broadcast the transaction using sync mode
resp, err := gm.broadcaster.BroadcastSync(ctx, tx.TxBytes)
if err != nil {
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast gasless transaction")
}
result := &BroadcastResult{
TxHash: resp.TxHash,
Height: resp.Height,
Code: resp.Code,
RawLog: resp.Log,
GasUsed: resp.GasUsed,
GasWanted: resp.GasWanted,
}
// Check for errors
if resp.Code != 0 {
return result, fmt.Errorf("transaction failed with code %d: %s", resp.Code, resp.Log)
}
return result, nil
}
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
func (gm *gaslessManager) IsEligibleForGasless(msgs []sdk.Msg) bool {
// Only single message transactions are eligible
if len(msgs) != 1 {
return false
}
// Check message type
msgType := sdk.MsgTypeURL(msgs[0])
// WebAuthn registration is gasless
if msgType == "/did.v1.MsgRegisterWebAuthnCredential" {
return true
}
// Future: Add other gasless message types here
return false
}
// EstimateGaslessGas estimates gas for a gasless transaction.
func (gm *gaslessManager) EstimateGaslessGas(msgType string) uint64 {
switch msgType {
case "/did.v1.MsgRegisterWebAuthnCredential":
return 200000 // Fixed gas for WebAuthn registration
default:
return 100000 // Default gas estimate
}
}
// generateAddressFromWebAuthn generates a deterministic address from WebAuthn credential.
func (gm *gaslessManager) generateAddressFromWebAuthn(credential *WebAuthnCredential) sdk.AccAddress {
// Use the credential ID as seed for address generation
// This ensures the same credential always generates the same address
// In production, this would use a proper derivation scheme
// For now, we'll use the first 20 bytes of the credential ID
addrBytes := make([]byte, 20)
copy(addrBytes, credential.RawID[:min(20, len(credential.RawID))])
return sdk.AccAddress(addrBytes)
}
// Helper function for min
func min(a, b int) int {
if a < b {
return a
}
return b
}
// WebAuthnGaslessClient provides a high-level interface for gasless WebAuthn operations.
type WebAuthnGaslessClient struct {
webauthnClient WebAuthnClient
gaslessManager GaslessTransactionManager
config *config.NetworkConfig
}
// NewWebAuthnGaslessClient creates a new WebAuthn gasless client.
func NewWebAuthnGaslessClient(
webauthnClient WebAuthnClient,
gaslessManager GaslessTransactionManager,
cfg *config.NetworkConfig,
) *WebAuthnGaslessClient {
return &WebAuthnGaslessClient{
webauthnClient: webauthnClient,
gaslessManager: gaslessManager,
config: cfg,
}
}
// RegisterGasless performs gasless WebAuthn registration.
func (wgc *WebAuthnGaslessClient) RegisterGasless(
ctx context.Context,
username string,
displayName string,
) (*GaslessRegistrationResult, error) {
// Create registration options
regOpts := &RegistrationOptions{
Username: username,
DisplayName: displayName,
Timeout: 60000,
UserVerification: "preferred",
AttestationType: "none",
}
// Begin WebAuthn registration
challenge, err := wgc.webauthnClient.BeginRegistration(ctx, regOpts)
if err != nil {
return nil, err
}
// Return challenge for browser to complete
// The actual credential will be created by the browser
result := &GaslessRegistrationResult{
Challenge: challenge,
GaslessEligible: true,
EstimatedGas: wgc.gaslessManager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential"),
}
return result, nil
}
// CompleteGaslessRegistration completes the gasless registration after browser response.
func (wgc *WebAuthnGaslessClient) CompleteGaslessRegistration(
ctx context.Context,
challenge *RegistrationChallenge,
response *AuthenticatorAttestationResponse,
username string,
autoCreateVault bool,
) (*BroadcastResult, error) {
// Complete WebAuthn registration to get credential
credential, err := wgc.webauthnClient.CompleteRegistration(ctx, challenge, response)
if err != nil {
// For now, create a mock credential since CompleteRegistration is not fully implemented
// In production, this would properly parse the attestation response
credential = &WebAuthnCredential{
ID: base64.URLEncoding.EncodeToString(response.AttestationObject[:32]),
RawID: response.AttestationObject[:32],
PublicKey: response.AttestationObject[32:64], // Mock public key
AttestationType: "none",
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: true,
},
Authenticator: &AuthenticatorData{
RPIDHash: challenge.Challenge,
},
UserID: string(challenge.User.ID),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
}
// Create gasless registration options
gaslessOpts := &GaslessRegistrationOptions{
Username: username,
AutoCreateVault: autoCreateVault,
WebAuthnChallenge: challenge.Challenge,
}
// Create gasless transaction
gaslessTx, err := wgc.gaslessManager.CreateGaslessRegistration(ctx, credential, gaslessOpts)
if err != nil {
return nil, err
}
// Broadcast gasless transaction
return wgc.gaslessManager.BroadcastGasless(ctx, gaslessTx)
}
// GaslessRegistrationResult contains the result of initiating gasless registration.
type GaslessRegistrationResult struct {
Challenge *RegistrationChallenge `json:"challenge"`
GaslessEligible bool `json:"gasless_eligible"`
EstimatedGas uint64 `json:"estimated_gas"`
}
// ValidateGaslessEligibility validates if a user is eligible for gasless transactions.
func ValidateGaslessEligibility(credential *WebAuthnCredential) error {
// Validate credential is not nil
if credential == nil {
return fmt.Errorf("credential cannot be nil")
}
// Validate credential has required fields
if len(credential.RawID) == 0 {
return fmt.Errorf("credential ID cannot be empty")
}
if len(credential.PublicKey) == 0 {
return fmt.Errorf("public key cannot be empty")
}
// Validate user presence and verification
if credential.Flags != nil {
if !credential.Flags.UserPresent {
return fmt.Errorf("user presence is required for gasless transactions")
}
}
return nil
}
// GetGaslessEndpoint returns the gasless transaction endpoint for the network.
func GetGaslessEndpoint(cfg *config.NetworkConfig) string {
// For now, gasless transactions use the same RPC endpoint
// In the future, this could be a separate endpoint
return cfg.RPC
}
+463
View File
@@ -0,0 +1,463 @@
package auth
import (
"context"
"encoding/base64"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/keys"
"github.com/sonr-io/sonr/client/tx"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// MockTxBuilder implements tx.TxBuilder for testing.
type MockTxBuilder struct {
messages []sdk.Msg
config *tx.TxConfig
}
func NewMockTxBuilder() *MockTxBuilder {
return &MockTxBuilder{
messages: make([]sdk.Msg, 0),
config: &tx.TxConfig{
ChainID: "test-chain",
GasPrice: 0.001,
GasDenom: "usnr",
GasLimit: 200000,
GasAdjustment: 1.5,
},
}
}
func (m *MockTxBuilder) WithChainID(chainID string) tx.TxBuilder {
m.config.ChainID = chainID
return m
}
func (m *MockTxBuilder) WithGasPrice(price float64, denom string) tx.TxBuilder {
m.config.GasPrice = price
m.config.GasDenom = denom
return m
}
func (m *MockTxBuilder) WithGasLimit(limit uint64) tx.TxBuilder {
m.config.GasLimit = limit
return m
}
func (m *MockTxBuilder) WithMemo(memo string) tx.TxBuilder {
m.config.Memo = memo
return m
}
func (m *MockTxBuilder) WithTimeoutHeight(height uint64) tx.TxBuilder {
m.config.TimeoutHeight = height
return m
}
func (m *MockTxBuilder) AddMessage(msg sdk.Msg) tx.TxBuilder {
m.messages = append(m.messages, msg)
return m
}
func (m *MockTxBuilder) AddMessages(msgs ...sdk.Msg) tx.TxBuilder {
m.messages = append(m.messages, msgs...)
return m
}
func (m *MockTxBuilder) ClearMessages() tx.TxBuilder {
m.messages = make([]sdk.Msg, 0)
return m
}
func (m *MockTxBuilder) WithFee(amount sdk.Coins) tx.TxBuilder {
m.config.Fee = amount
return m
}
func (m *MockTxBuilder) WithGasAdjustment(adjustment float64) tx.TxBuilder {
m.config.GasAdjustment = adjustment
return m
}
func (m *MockTxBuilder) EstimateGas(ctx context.Context) (uint64, error) {
return 200000, nil
}
func (m *MockTxBuilder) Sign(ctx context.Context, keyring keys.KeyringManager) (*tx.SignedTx, error) {
unsignedTx, _ := m.Build()
return &tx.SignedTx{
UnsignedTx: unsignedTx,
Signature: []byte("mock-signature"),
PubKey: []byte("mock-pubkey"),
TxBytes: []byte("mock-tx-bytes"),
}, nil
}
func (m *MockTxBuilder) SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*tx.BroadcastResult, error) {
return &tx.BroadcastResult{
TxHash: "mock-tx-hash",
Height: 12345,
Code: 0,
Log: "success",
GasUsed: 100000,
GasWanted: 200000,
}, nil
}
func (m *MockTxBuilder) Broadcast(ctx context.Context, signedTx *tx.SignedTx) (*tx.BroadcastResult, error) {
return &tx.BroadcastResult{
TxHash: "mock-tx-hash",
Height: 12345,
Code: 0,
Log: "success",
GasUsed: 100000,
GasWanted: 200000,
}, nil
}
func (m *MockTxBuilder) Simulate(ctx context.Context) (*tx.SimulateResult, error) {
return &tx.SimulateResult{
GasWanted: 200000,
GasUsed: 100000,
Log: "simulation success",
}, nil
}
func (m *MockTxBuilder) Build() (*tx.UnsignedTx, error) {
return &tx.UnsignedTx{
Messages: m.messages,
Config: m.config,
SignBytes: []byte("mock-sign-bytes"),
}, nil
}
func (m *MockTxBuilder) BuildSigned(signature []byte, pubKey []byte) (*tx.SignedTx, error) {
unsignedTx, _ := m.Build()
return &tx.SignedTx{
UnsignedTx: unsignedTx,
Signature: signature,
PubKey: pubKey,
TxBytes: append(unsignedTx.SignBytes, signature...),
}, nil
}
func (m *MockTxBuilder) Config() *tx.TxConfig {
return m.config
}
// MockBroadcaster implements tx.Broadcaster for testing.
type MockBroadcaster struct {
broadcastedTxs [][]byte
shouldFail bool
}
func NewMockBroadcaster() *MockBroadcaster {
return &MockBroadcaster{
broadcastedTxs: make([][]byte, 0),
shouldFail: false,
}
}
func (m *MockBroadcaster) Broadcast(ctx context.Context, txBytes []byte, mode tx.BroadcastMode) (*tx.BroadcastResult, error) {
m.broadcastedTxs = append(m.broadcastedTxs, txBytes)
if m.shouldFail {
return &tx.BroadcastResult{
Code: 1,
Log: "mock error",
}, nil
}
return &tx.BroadcastResult{
TxHash: "mock-tx-hash",
Height: 12345,
Code: 0,
Log: "success",
GasUsed: 100000,
GasWanted: 200000,
}, nil
}
func (m *MockBroadcaster) BroadcastSync(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, tx.BroadcastModeSync)
}
func (m *MockBroadcaster) BroadcastAsync(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, tx.BroadcastModeAsync)
}
func (m *MockBroadcaster) BroadcastBlock(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, tx.BroadcastModeBlock)
}
func (m *MockBroadcaster) BroadcastWithRetry(ctx context.Context, txBytes []byte, mode tx.BroadcastMode, maxRetries int) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, mode)
}
func (m *MockBroadcaster) WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*tx.TxConfirmation, error) {
return &tx.TxConfirmation{
TxHash: txHash,
BlockHeight: 12345,
BlockTime: time.Now(),
Code: 0,
Log: "confirmed",
GasWanted: 200000,
GasUsed: 100000,
}, nil
}
func (m *MockBroadcaster) WithRetryConfig(config tx.RetryConfig) tx.Broadcaster {
return m
}
func (m *MockBroadcaster) WithTimeout(timeout time.Duration) tx.Broadcaster {
return m
}
// GaslessTestSuite tests gasless transaction functionality.
type GaslessTestSuite struct {
suite.Suite
manager GaslessTransactionManager
txBuilder tx.TxBuilder
broadcaster tx.Broadcaster
config *config.NetworkConfig
}
func (suite *GaslessTestSuite) SetupTest() {
cfg := config.LocalNetwork()
suite.config = &cfg
suite.txBuilder = NewMockTxBuilder()
suite.broadcaster = NewMockBroadcaster()
suite.manager = NewGaslessTransactionManager(
suite.txBuilder,
suite.broadcaster,
suite.config,
)
}
func (suite *GaslessTestSuite) TestCreateGaslessRegistration() {
// Create test WebAuthn credential
credential := &WebAuthnCredential{
ID: "test-credential-id",
RawID: []byte("test-raw-id"),
PublicKey: []byte("test-public-key"),
AttestationType: "none",
Transports: []string{"usb"},
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: true,
},
Authenticator: &AuthenticatorData{
RPIDHash: []byte("test-rp-id-hash"),
},
}
// Create options
opts := &GaslessRegistrationOptions{
Username: "testuser",
AutoCreateVault: true,
WebAuthnChallenge: []byte("test-challenge"),
}
// Create gasless registration
gaslessTx, err := suite.manager.CreateGaslessRegistration(context.Background(), credential, opts)
suite.Require().NoError(err)
suite.Require().NotNil(gaslessTx)
// Verify transaction fields
suite.Equal("WebAuthn Gasless Registration", gaslessTx.Memo)
suite.Equal(uint64(200000), gaslessTx.GasLimit)
suite.Equal(signing.SignMode_SIGN_MODE_DIRECT, gaslessTx.SignMode)
suite.Len(gaslessTx.Messages, 1)
// Verify message type
msg, ok := gaslessTx.Messages[0].(*didtypes.MsgRegisterWebAuthnCredential)
suite.Require().True(ok)
suite.Equal("testuser", msg.Username)
suite.True(msg.AutoCreateVault)
}
func (suite *GaslessTestSuite) TestBroadcastGasless() {
// Create test transaction
gaslessTx := &GaslessTransaction{
Messages: []sdk.Msg{
&didtypes.MsgRegisterWebAuthnCredential{
Controller: "sonr1xyz...",
Username: "testuser",
},
},
Memo: "Test Gasless",
GasLimit: 200000,
SignerAddress: "sonr1xyz...",
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
TxBytes: []byte("test-tx-bytes"),
}
// Broadcast transaction
result, err := suite.manager.BroadcastGasless(context.Background(), gaslessTx)
suite.Require().NoError(err)
suite.Require().NotNil(result)
// Verify result
suite.Equal("mock-tx-hash", result.TxHash)
suite.Equal(int64(12345), result.Height)
suite.Equal(uint32(0), result.Code)
}
func (suite *GaslessTestSuite) TestIsEligibleForGasless() {
// Test WebAuthn registration message
webauthnMsg := &didtypes.MsgRegisterWebAuthnCredential{
Controller: "sonr1xyz...",
Username: "testuser",
}
// Should be eligible
suite.True(suite.manager.IsEligibleForGasless([]sdk.Msg{webauthnMsg}))
// Multiple messages should not be eligible
suite.False(suite.manager.IsEligibleForGasless([]sdk.Msg{webauthnMsg, webauthnMsg}))
// Other message types should not be eligible
otherMsg := &didtypes.MsgCreateDID{
Controller: "sonr1xyz...",
}
suite.False(suite.manager.IsEligibleForGasless([]sdk.Msg{otherMsg}))
}
func (suite *GaslessTestSuite) TestEstimateGaslessGas() {
// Test WebAuthn registration gas estimate
gas := suite.manager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential")
suite.Equal(uint64(200000), gas)
// Test default gas estimate
gas = suite.manager.EstimateGaslessGas("/unknown.message.type")
suite.Equal(uint64(100000), gas)
}
func TestGaslessTestSuite(t *testing.T) {
suite.Run(t, new(GaslessTestSuite))
}
// TestValidateGaslessEligibility tests credential validation.
func TestValidateGaslessEligibility(t *testing.T) {
tests := []struct {
name string
credential *WebAuthnCredential
wantError bool
}{
{
name: "valid credential",
credential: &WebAuthnCredential{
RawID: []byte("test-id"),
PublicKey: []byte("test-key"),
Flags: &AuthenticatorFlags{
UserPresent: true,
},
},
wantError: false,
},
{
name: "nil credential",
credential: nil,
wantError: true,
},
{
name: "empty credential ID",
credential: &WebAuthnCredential{
PublicKey: []byte("test-key"),
Flags: &AuthenticatorFlags{
UserPresent: true,
},
},
wantError: true,
},
{
name: "empty public key",
credential: &WebAuthnCredential{
RawID: []byte("test-id"),
Flags: &AuthenticatorFlags{
UserPresent: true,
},
},
wantError: true,
},
{
name: "no user presence",
credential: &WebAuthnCredential{
RawID: []byte("test-id"),
PublicKey: []byte("test-key"),
Flags: &AuthenticatorFlags{
UserPresent: false,
},
},
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateGaslessEligibility(tt.credential)
if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// TestGetGaslessEndpoint tests endpoint retrieval.
func TestGetGaslessEndpoint(t *testing.T) {
cfg := &config.NetworkConfig{
RPC: "http://localhost:26657",
}
endpoint := GetGaslessEndpoint(cfg)
require.Equal(t, "http://localhost:26657", endpoint)
}
// TestWebAuthnCredentialConversion tests conversion between credential types.
func TestWebAuthnCredentialConversion(t *testing.T) {
// Create client credential
clientCred := &WebAuthnCredential{
ID: base64.RawURLEncoding.EncodeToString([]byte("test-id")),
RawID: []byte("test-id"),
PublicKey: []byte("test-public-key"),
AttestationType: "none",
Transports: []string{"usb", "nfc"},
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: false,
},
}
// Convert to protobuf credential
protoCred := didtypes.WebAuthnCredential{
CredentialId: base64.RawURLEncoding.EncodeToString(clientCred.RawID),
PublicKey: clientCred.PublicKey,
AttestationType: clientCred.AttestationType,
Origin: "http://localhost",
Algorithm: -7, // ES256
CreatedAt: time.Now().Unix(),
RpId: "localhost",
RpName: "Sonr Local",
Transports: clientCred.Transports,
UserVerified: clientCred.Flags.UserVerified,
}
// Verify conversion
require.Equal(t, base64.RawURLEncoding.EncodeToString([]byte("test-id")), protoCred.CredentialId)
require.Equal(t, clientCred.PublicKey, protoCred.PublicKey)
require.Equal(t, clientCred.AttestationType, protoCred.AttestationType)
require.Equal(t, clientCred.Transports, protoCred.Transports)
require.Equal(t, clientCred.Flags.UserVerified, protoCred.UserVerified)
}
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
package auth
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWebAuthnClient_BasicRegistration(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
rpName: "Sonr",
}
// Test BeginRegistration
opts := &RegistrationOptions{
UserID: "test_user",
Username: "testuser",
DisplayName: "Test User",
}
challenge, err := client.BeginRegistration(context.Background(), opts)
require.NoError(t, err)
assert.NotNil(t, challenge)
assert.NotEmpty(t, challenge.Challenge)
assert.Equal(t, "localhost", challenge.RelyingParty.ID)
assert.Equal(t, "Sonr", challenge.RelyingParty.Name)
assert.Equal(t, []byte("test_user"), challenge.User.ID)
assert.Equal(t, "testuser", challenge.User.Name)
assert.Equal(t, "Test User", challenge.User.DisplayName)
}
func TestWebAuthnClient_BasicAuthentication(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
}
// Test BeginAuthentication
opts := &AuthenticationOptions{
UserVerification: "preferred",
Timeout: 60000,
}
challenge, err := client.BeginAuthentication(context.Background(), opts)
require.NoError(t, err)
assert.NotNil(t, challenge)
assert.NotEmpty(t, challenge.Challenge)
assert.Equal(t, "localhost", challenge.RelyingPartyID)
assert.Equal(t, "preferred", challenge.UserVerification)
assert.Equal(t, 60000, challenge.Timeout)
}
func TestWebAuthnClient_VerifyClientData(t *testing.T) {
challenge := []byte("test_challenge")
origin := "http://localhost"
// Create valid client data
clientData := map[string]any{
"type": "webauthn.create",
"challenge": "dGVzdF9jaGFsbGVuZ2U", // base64url encoded "test_challenge"
"origin": origin,
}
clientDataJSON, _ := json.Marshal(clientData)
// Test successful verification
result, err := verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "webauthn.create", string(result.Type))
assert.Equal(t, origin, result.Origin)
// Test wrong type
clientData["type"] = "webauthn.get"
clientDataJSON, _ = json.Marshal(clientData)
_, err = verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
assert.Error(t, err)
// Test wrong origin
clientData["type"] = "webauthn.create"
clientData["origin"] = "http://evil.com"
clientDataJSON, _ = json.Marshal(clientData)
_, err = verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
assert.Error(t, err)
// Test wrong challenge
clientData["origin"] = origin
clientData["challenge"] = "d3JvbmdfY2hhbGxlbmdl" // base64url encoded "wrong_challenge"
clientDataJSON, _ = json.Marshal(clientData)
_, err = verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
assert.Error(t, err)
}
+338
View File
@@ -0,0 +1,338 @@
package auth
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/x/did/types"
)
// Mock DIDClient for testing
type mockDIDClient struct {
credentials []*types.WebAuthnCredential
didDoc *types.DIDDocument
error error
}
func (m *mockDIDClient) QueryCredentials(ctx context.Context, did string) ([]*types.WebAuthnCredential, error) {
if m.error != nil {
return nil, m.error
}
return m.credentials, nil
}
func (m *mockDIDClient) QueryCredential(ctx context.Context, did, credentialID string) (*types.WebAuthnCredential, error) {
if m.error != nil {
return nil, m.error
}
for _, cred := range m.credentials {
if cred.CredentialId == credentialID {
return cred, nil
}
}
return nil, fmt.Errorf("key not found")
}
func (m *mockDIDClient) UpdateCredential(ctx context.Context, did string, credential *types.WebAuthnCredential) error {
if m.error != nil {
return m.error
}
for i, cred := range m.credentials {
if cred.CredentialId == credential.CredentialId {
m.credentials[i] = credential
return nil
}
}
return fmt.Errorf("key not found")
}
func (m *mockDIDClient) RevokeCredential(ctx context.Context, did, credentialID string) error {
if m.error != nil {
return m.error
}
for i, cred := range m.credentials {
if cred.CredentialId == credentialID {
// Remove from slice
m.credentials = append(m.credentials[:i], m.credentials[i+1:]...)
return nil
}
}
return fmt.Errorf("key not found")
}
func (m *mockDIDClient) QueryDIDDocument(ctx context.Context, did string) (*types.DIDDocument, error) {
if m.error != nil {
return nil, m.error
}
return m.didDoc, nil
}
func (m *mockDIDClient) AuthenticateWithDID(ctx context.Context, did string, assertion []byte) (bool, error) {
if m.error != nil {
return false, m.error
}
return true, nil
}
func (m *mockDIDClient) SignTransaction(ctx context.Context, did string, txBytes []byte, credentialID string) ([]byte, error) {
if m.error != nil {
return nil, m.error
}
// Mock signature
return []byte("mock_signature"), nil
}
// Helper function to create test credentials
func createTestCredential(id string) *types.WebAuthnCredential {
return &types.WebAuthnCredential{
CredentialId: id,
RawId: base64.RawURLEncoding.EncodeToString([]byte(id)),
ClientDataJson: `{"type":"webauthn.create","challenge":"test","origin":"http://localhost"}`,
AttestationObject: base64.RawURLEncoding.EncodeToString([]byte("test_attestation")),
PublicKey: []byte("test_public_key"),
Algorithm: -7, // ES256
Origin: "http://localhost",
CreatedAt: 1234567890,
}
}
func TestWebAuthnClient_CompleteRegistration(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
}
challenge := &RegistrationChallenge{
Challenge: []byte("test_challenge"),
User: &User{
ID: []byte("test_user"),
Name: "Test User",
DisplayName: "Test",
},
}
// Create valid client data JSON
clientData := map[string]any{
"type": "webauthn.create",
"challenge": challenge.Challenge,
"origin": "http://localhost",
}
clientDataJSON, _ := json.Marshal(clientData)
response := &AuthenticatorAttestationResponse{
ClientDataJSON: clientDataJSON,
AttestationObject: []byte("test_attestation"),
}
// Test successful registration
cred, err := client.CompleteRegistration(context.Background(), challenge, response)
assert.NoError(t, err)
assert.NotNil(t, cred)
// Test with invalid client data
response.ClientDataJSON = []byte("invalid_json")
_, err = client.CompleteRegistration(context.Background(), challenge, response)
assert.Error(t, err)
}
func TestWebAuthnClient_CompleteAuthentication(t *testing.T) {
credential := createTestCredential("test_cred_1")
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
}
challenge := &AuthenticationChallenge{
Challenge: []byte("test_challenge"),
}
// Create valid client data JSON
clientData := map[string]any{
"type": "webauthn.get",
"challenge": challenge.Challenge,
"origin": "http://localhost",
}
clientDataJSON, _ := json.Marshal(clientData)
response := &AuthenticatorAssertionResponse{
ClientDataJSON: clientDataJSON,
AuthenticatorData: []byte("test_auth_data"),
Signature: []byte("test_signature"),
UserHandle: []byte("test_user"),
}
// Test successful authentication
result, err := client.CompleteAuthentication(context.Background(), challenge, response, "test_cred_1")
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Verified)
// Test with wrong origin
clientData["origin"] = "http://evil.com"
clientDataJSON, _ = json.Marshal(clientData)
response.ClientDataJSON = base64.RawURLEncoding.EncodeToString(clientDataJSON)
_, err = client.CompleteAuthentication(context.Background(), challenge, response, "test_cred_1")
assert.Error(t, err)
}
func TestWebAuthnClient_ListCredentials(t *testing.T) {
cred1 := createTestCredential("cred1")
cred2 := createTestCredential("cred2")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred1, cred2},
},
}
// Test successful list
creds, err := client.ListCredentials(context.Background(), "did:test:123")
require.NoError(t, err)
assert.Len(t, creds, 2)
assert.Equal(t, "cred1", creds[0].CredentialId)
assert.Equal(t, "cred2", creds[1].CredentialId)
// Test with error
client.didClient = &mockDIDClient{error: assert.AnError}
_, err = client.ListCredentials(context.Background(), "did:test:123")
assert.Error(t, err)
}
func TestWebAuthnClient_GetCredential(t *testing.T) {
cred := createTestCredential("test_cred")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred},
},
}
// Test successful get
retrieved, err := client.GetCredential(context.Background(), "did:test:123", "test_cred")
require.NoError(t, err)
assert.Equal(t, cred.CredentialId, retrieved.CredentialId)
// Test credential not found
_, err = client.GetCredential(context.Background(), "did:test:123", "non_existent")
assert.Error(t, err)
}
func TestWebAuthnClient_UpdateCredential(t *testing.T) {
cred := createTestCredential("test_cred")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred},
},
}
// Update the credential
updatedCred := createTestCredential("test_cred")
updatedCred.Origin = "https://updated.example.com"
err := client.UpdateCredential(context.Background(), "did:test:123", updatedCred)
require.NoError(t, err)
// Verify update
mock := client.didClient.(*mockDIDClient)
assert.Equal(t, "https://updated.example.com", mock.credentials[0].Origin)
// Test update non-existent credential
nonExistent := createTestCredential("non_existent")
err = client.UpdateCredential(context.Background(), "did:test:123", nonExistent)
assert.Error(t, err)
}
func TestWebAuthnClient_RevokeCredential(t *testing.T) {
cred1 := createTestCredential("cred1")
cred2 := createTestCredential("cred2")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred1, cred2},
},
}
// Revoke first credential
err := client.RevokeCredential(context.Background(), "did:test:123", "cred1")
require.NoError(t, err)
// Verify revocation
mock := client.didClient.(*mockDIDClient)
assert.Len(t, mock.credentials, 1)
assert.Equal(t, "cred2", mock.credentials[0].CredentialId)
// Test revoke non-existent credential
err = client.RevokeCredential(context.Background(), "did:test:123", "non_existent")
assert.Error(t, err)
}
func TestWebAuthnClient_AuthenticateWithDID(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
didClient: &mockDIDClient{
didDoc: &types.DIDDocument{
Id: "did:test:123",
},
},
}
assertion := &AuthenticationAssertion{
CredentialID: "test_cred",
ClientDataJSON: base64.RawURLEncoding.EncodeToString([]byte(`{"type":"webauthn.get"}`)),
AuthenticatorData: base64.RawURLEncoding.EncodeToString([]byte("auth_data")),
Signature: base64.RawURLEncoding.EncodeToString([]byte("signature")),
UserHandle: base64.RawURLEncoding.EncodeToString([]byte("user")),
}
// Test successful authentication
result, err := client.AuthenticateWithDID(context.Background(), "did:test:123", assertion)
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Success)
assert.Equal(t, "test_cred", result.CredentialId)
// Test with error
client.didClient = &mockDIDClient{error: assert.AnError}
_, err = client.AuthenticateWithDID(context.Background(), "did:test:123", assertion)
assert.Error(t, err)
}
func TestWebAuthnClient_SignWithWebAuthn(t *testing.T) {
client := &webAuthnClient{
didClient: &mockDIDClient{},
}
txData := []byte("transaction_data")
// Test successful signing
sig, err := client.SignWithWebAuthn(context.Background(), "did:test:123", txData, "test_cred")
require.NoError(t, err)
assert.Equal(t, []byte("mock_signature"), sig)
// Test with error
client.didClient = &mockDIDClient{error: assert.AnError}
_, err = client.SignWithWebAuthn(context.Background(), "did:test:123", txData, "test_cred")
assert.Error(t, err)
}
func TestWebAuthnClient_BindCredential(t *testing.T) {
client := &webAuthnClient{}
cred := createTestCredential("test_cred")
// Test that BindCredential returns the existing implementation message
result, err := client.BindCredential(context.Background(), "did:test:123", cred)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "test_cred", result.CredentialId)
// The actual binding logic would be implemented in a later phase
// For now, it just returns the credential as-is
}