Files
sonr/client/auth/webauthn.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

1103 lines
40 KiB
Go

// Package auth provides WebAuthn integration for the Sonr client SDK.
package auth
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"time"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/keys"
"github.com/sonr-io/common/webauthn"
"github.com/sonr-io/common/webauthn/webauthncbor"
"github.com/sonr-io/common/webauthn/webauthncose"
)
// WebAuthnClient provides an interface for WebAuthn operations with Sonr's Decentralized Abstracted Smart Wallets.
type WebAuthnClient interface {
// Registration Operations
BeginRegistration(ctx context.Context, opts *RegistrationOptions) (*RegistrationChallenge, error)
CompleteRegistration(ctx context.Context, challenge *RegistrationChallenge, response *AuthenticatorAttestationResponse) (*WebAuthnCredential, error)
// Authentication Operations
BeginAuthentication(ctx context.Context, opts *AuthenticationOptions) (*AuthenticationChallenge, error)
CompleteAuthentication(ctx context.Context, challenge *AuthenticationChallenge, response *AuthenticatorAssertionResponse, credentialID string) (*AuthenticationResult, error)
// Credential Management
ListCredentials(ctx context.Context, userID string) ([]*WebAuthnCredential, error)
GetCredential(ctx context.Context, credentialID string) (*WebAuthnCredential, error)
UpdateCredential(ctx context.Context, credentialID string, opts *UpdateCredentialOptions) (*WebAuthnCredential, error)
RevokeCredential(ctx context.Context, credentialID string) error
// DID Integration
RegisterWithDID(ctx context.Context, did string, opts *DIDRegistrationOptions) (*DIDWebAuthnBinding, error)
AuthenticateWithDID(ctx context.Context, did string, opts *DIDAuthenticationOptions) (*DIDAuthenticationResult, error)
// Wallet Integration
BindToWallet(ctx context.Context, credentialID string, keyring keys.KeyringManager) (*WalletBinding, error)
SignWithWebAuthn(ctx context.Context, credentialID string, data []byte) (*WebAuthnSignature, error)
}
// RegistrationOptions configures WebAuthn registration.
type RegistrationOptions struct {
UserID string `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Timeout int `json:"timeout,omitempty"` // Timeout in milliseconds
UserVerification string `json:"user_verification,omitempty"` // required, preferred, discouraged
AttestationType string `json:"attestation_type,omitempty"` // none, indirect, direct
AuthenticatorSelection *AuthenticatorSelection `json:"authenticator_selection,omitempty"`
Extensions map[string]any `json:"extensions,omitempty"`
}
// AuthenticatorSelection specifies authenticator requirements.
type AuthenticatorSelection struct {
AuthenticatorAttachment string `json:"authenticator_attachment,omitempty"` // platform, cross-platform
RequireResidentKey bool `json:"require_resident_key,omitempty"`
UserVerification string `json:"user_verification,omitempty"`
}
// RegistrationChallenge contains the challenge for registration.
type RegistrationChallenge struct {
Challenge []byte `json:"challenge"`
RelyingParty *RelyingParty `json:"relying_party"`
User *User `json:"user"`
PubKeyCredParams []*PubKeyCredParam `json:"pub_key_cred_params"`
Timeout int `json:"timeout"`
ExcludeCredentials []*CredentialDescriptor `json:"exclude_credentials,omitempty"`
AuthenticatorSelection *AuthenticatorSelection `json:"authenticator_selection,omitempty"`
Attestation string `json:"attestation"`
Extensions map[string]any `json:"extensions,omitempty"`
}
// RelyingParty represents the relying party information.
type RelyingParty struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon,omitempty"`
}
// User represents the user information for WebAuthn.
type User struct {
ID []byte `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Icon string `json:"icon,omitempty"`
}
// PubKeyCredParam specifies the public key parameters.
type PubKeyCredParam struct {
Type string `json:"type"`
Algorithm int `json:"alg"`
}
// CredentialDescriptor describes a credential.
type CredentialDescriptor struct {
ID []byte `json:"id"`
Type string `json:"type"`
Transports []string `json:"transports,omitempty"`
}
// AuthenticatorAttestationResponse contains the registration response from the authenticator.
type AuthenticatorAttestationResponse struct {
ClientDataJSON []byte `json:"client_data_json"`
AttestationObject []byte `json:"attestation_object"`
Transports []string `json:"transports,omitempty"`
}
// WebAuthnCredential represents a stored WebAuthn credential.
type WebAuthnCredential struct {
ID string `json:"id"`
RawID []byte `json:"raw_id"`
PublicKey []byte `json:"public_key"`
Algorithm int64 `json:"algorithm"`
AttestationType string `json:"attestation_type"`
Transports []string `json:"transports"`
Flags *AuthenticatorFlags `json:"flags"`
Authenticator *AuthenticatorData `json:"authenticator"`
Counter uint32 `json:"counter"`
AAGUID []byte `json:"aaguid"`
UserID string `json:"user_id"`
UserVerified bool `json:"user_verified"`
BackupEligible bool `json:"backup_eligible"`
BackupState bool `json:"backup_state"`
Origin string `json:"origin"`
CreatedAt string `json:"created_at"`
LastUsed string `json:"last_used,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
// AuthenticatorFlags represents authenticator flags.
type AuthenticatorFlags struct {
UserPresent bool `json:"user_present"`
UserVerified bool `json:"user_verified"`
AttestedData bool `json:"attested_data"`
ExtensionData bool `json:"extension_data"`
}
// AuthenticatorData contains authenticator data.
type AuthenticatorData struct {
RPIDHash []byte `json:"rpid_hash"`
Flags byte `json:"flags"`
Counter uint32 `json:"counter"`
AttestedData []byte `json:"attested_data,omitempty"`
ExtensionData []byte `json:"extension_data,omitempty"`
}
// AuthenticationOptions configures WebAuthn authentication.
type AuthenticationOptions struct {
UserID string `json:"user_id,omitempty"`
Timeout int `json:"timeout,omitempty"`
UserVerification string `json:"user_verification,omitempty"`
AllowedCredentials []*CredentialDescriptor `json:"allowed_credentials,omitempty"`
Extensions map[string]any `json:"extensions,omitempty"`
}
// AuthenticationChallenge contains the challenge for authentication.
type AuthenticationChallenge struct {
Challenge []byte `json:"challenge"`
Timeout int `json:"timeout"`
RelyingPartyID string `json:"relying_party_id"`
AllowedCredentials []*CredentialDescriptor `json:"allowed_credentials,omitempty"`
UserVerification string `json:"user_verification"`
Extensions map[string]any `json:"extensions,omitempty"`
}
// AuthenticatorAssertionResponse contains the authentication response from the authenticator.
type AuthenticatorAssertionResponse struct {
ClientDataJSON []byte `json:"client_data_json"`
AuthenticatorData []byte `json:"authenticator_data"`
Signature []byte `json:"signature"`
UserHandle []byte `json:"user_handle,omitempty"`
}
// AuthenticationResult contains the result of authentication.
type AuthenticationResult struct {
Success bool `json:"success"`
Verified bool `json:"verified"`
Credential *WebAuthnCredential `json:"credential,omitempty"`
Counter uint32 `json:"counter"`
UserHandle []byte `json:"user_handle,omitempty"`
Error string `json:"error,omitempty"`
}
// UpdateCredentialOptions configures credential updates.
type UpdateCredentialOptions struct {
Metadata map[string]any `json:"metadata,omitempty"`
}
// DIDRegistrationOptions configures DID-based WebAuthn registration.
type DIDRegistrationOptions struct {
CredentialOptions *RegistrationOptions `json:"credential_options"`
DIDDocument map[string]any `json:"did_document,omitempty"`
VerificationMethod string `json:"verification_method,omitempty"`
}
// DIDWebAuthnBinding represents a binding between a DID and WebAuthn credential.
type DIDWebAuthnBinding struct {
DID string `json:"did"`
CredentialID string `json:"credential_id"`
Credential *WebAuthnCredential `json:"credential"`
VerificationMethod string `json:"verification_method"`
CreatedAt string `json:"created_at"`
}
// DIDDocument represents a minimal DID document structure.
type DIDDocument struct {
ID string `json:"id"`
VerificationMethod []map[string]any `json:"verificationMethod,omitempty"`
Authentication []any `json:"authentication,omitempty"`
}
// DIDAuthenticationOptions configures DID-based authentication.
type DIDAuthenticationOptions struct {
AuthenticationOptions *AuthenticationOptions `json:"authentication_options"`
Challenge []byte `json:"challenge,omitempty"`
}
// DIDAuthenticationResult contains the result of DID authentication.
type DIDAuthenticationResult struct {
Success bool `json:"success"`
DID string `json:"did"`
Challenge *AuthenticationChallenge `json:"challenge,omitempty"`
CredentialOptions []*WebAuthnCredential `json:"credential_options,omitempty"`
SessionID string `json:"session_id,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
AuthenticationResult *AuthenticationResult `json:"authentication_result,omitempty"`
WalletIdentity *keys.WalletIdentity `json:"wallet_identity,omitempty"`
}
// WalletBinding represents a binding between a WebAuthn credential and a wallet.
type WalletBinding struct {
CredentialID string `json:"credential_id"`
WalletIdentity *keys.WalletIdentity `json:"wallet_identity"`
BindingType string `json:"binding_type"` // primary, secondary, recovery
CreatedAt string `json:"created_at"`
}
// WebAuthnSignature represents a signature created using WebAuthn.
type WebAuthnSignature struct {
Signature []byte `json:"signature"`
CredentialID string `json:"credential_id"`
Counter uint32 `json:"counter"`
AuthenticatorData []byte `json:"authenticator_data"`
ClientDataJSON []byte `json:"client_data_json"`
}
// webAuthnClient implements the WebAuthnClient interface.
type webAuthnClient struct {
keyring keys.KeyringManager
rpID string
rpName string
origin string
pendingChallenges map[string]*AuthenticationChallenge
pendingSignatures map[string][]byte
}
// NewWebAuthnClient creates a new WebAuthn client.
func NewWebAuthnClient(keyring keys.KeyringManager, rpID, rpName string) WebAuthnClient {
return &webAuthnClient{
keyring: keyring,
rpID: rpID,
rpName: rpName,
origin: fmt.Sprintf("https://%s", rpID),
pendingChallenges: make(map[string]*AuthenticationChallenge),
pendingSignatures: make(map[string][]byte),
}
}
// BeginRegistration initiates WebAuthn registration.
func (w *webAuthnClient) BeginRegistration(ctx context.Context, opts *RegistrationOptions) (*RegistrationChallenge, error) {
// Generate challenge
challenge := make([]byte, 32)
if _, err := rand.Read(challenge); err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to generate challenge")
}
// Create user ID if not provided
userID := opts.UserID
if userID == "" {
userIDBytes := make([]byte, 16)
rand.Read(userIDBytes)
userID = base64.URLEncoding.EncodeToString(userIDBytes)
}
// Build registration challenge
regChallenge := &RegistrationChallenge{
Challenge: challenge,
RelyingParty: &RelyingParty{
ID: w.rpID,
Name: w.rpName,
},
User: &User{
ID: []byte(userID),
Name: opts.Username,
DisplayName: opts.DisplayName,
},
PubKeyCredParams: []*PubKeyCredParam{
{Type: "public-key", Algorithm: -7}, // ES256
{Type: "public-key", Algorithm: -257}, // RS256
},
Timeout: opts.Timeout,
AuthenticatorSelection: opts.AuthenticatorSelection,
Attestation: "none",
Extensions: opts.Extensions,
}
if regChallenge.Timeout == 0 {
regChallenge.Timeout = 60000 // 60 seconds default
}
return regChallenge, nil
}
// CompleteRegistration completes WebAuthn registration.
func (w *webAuthnClient) CompleteRegistration(ctx context.Context, challenge *RegistrationChallenge, response *AuthenticatorAttestationResponse) (*WebAuthnCredential, error) {
// Verify client data JSON
clientData, err := verifyClientData(response.ClientDataJSON, challenge.Challenge, "webauthn.create", w.origin)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "client data verification failed")
}
// Parse attestation object
attestationObj, err := parseAttestationObject(response.AttestationObject)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "attestation object parsing failed")
}
// Verify authenticator data
if err := verifyAuthenticatorData(attestationObj.AuthData, w.rpID); err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "authenticator data verification failed")
}
// Extract public key from authenticator data
if len(attestationObj.AuthData.AttData.CredentialID) == 0 {
return nil, errors.NewModuleError("auth", "CompleteRegistration",
fmt.Errorf("no attestation data in authenticator response"))
}
// Parse COSE public key
publicKey, err := parseCOSEPublicKey(attestationObj.AuthData.AttData.CredentialPublicKey)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "public key parsing failed")
}
// Verify attestation (if present)
if attestationObj.Format != "none" {
clientDataHash := sha256.Sum256(response.ClientDataJSON)
if err := verifyAttestation(attestationObj, clientDataHash[:]); err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "attestation verification failed")
}
}
// Create credential for storage
credential := &WebAuthnCredential{
ID: base64.URLEncoding.EncodeToString(attestationObj.AuthData.AttData.CredentialID),
PublicKey: attestationObj.AuthData.AttData.CredentialPublicKey,
Algorithm: publicKey.Algorithm,
AttestationType: attestationObj.Format,
Transports: response.Transports,
Counter: attestationObj.AuthData.Counter,
AAGUID: attestationObj.AuthData.AttData.AAGUID,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
LastUsed: time.Now().UTC().Format(time.RFC3339),
UserVerified: attestationObj.AuthData.Flags.UserVerified(),
BackupEligible: attestationObj.AuthData.Flags.HasBackupEligible(),
BackupState: attestationObj.AuthData.Flags.HasBackupState(),
Origin: clientData.Origin,
}
// Store credential via DID module (implementation would interact with blockchain)
// This would typically involve creating a MsgRegisterWebAuthnCredential transaction
// For now, we return the credential object
return credential, nil
}
// BeginAuthentication initiates WebAuthn authentication.
func (w *webAuthnClient) BeginAuthentication(ctx context.Context, opts *AuthenticationOptions) (*AuthenticationChallenge, error) {
// Generate challenge
challenge := make([]byte, 32)
if _, err := rand.Read(challenge); err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to generate challenge")
}
authChallenge := &AuthenticationChallenge{
Challenge: challenge,
Timeout: opts.Timeout,
RelyingPartyID: w.rpID,
AllowedCredentials: opts.AllowedCredentials,
UserVerification: opts.UserVerification,
Extensions: opts.Extensions,
}
if authChallenge.Timeout == 0 {
authChallenge.Timeout = 60000 // 60 seconds default
}
if authChallenge.UserVerification == "" {
authChallenge.UserVerification = "preferred"
}
return authChallenge, nil
}
// CompleteAuthentication completes WebAuthn authentication.
func (w *webAuthnClient) CompleteAuthentication(ctx context.Context, challenge *AuthenticationChallenge, response *AuthenticatorAssertionResponse, credentialID string) (*AuthenticationResult, error) {
// Verify client data JSON
_, err := verifyClientData(response.ClientDataJSON, challenge.Challenge, "webauthn.get", w.origin)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "client data verification failed")
}
// Parse authenticator data
var authData webauthn.AuthenticatorData
if err := authData.Unmarshal(response.AuthenticatorData); err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "authenticator data parsing failed")
}
// Verify RP ID hash
rpIDHash := sha256.Sum256([]byte(w.rpID))
if !bytes.Equal(authData.RPIDHash[:], rpIDHash[:]) {
return nil, errors.NewModuleError("auth", "CompleteAuthentication",
fmt.Errorf("RP ID hash mismatch"))
}
// Verify user presence
if !authData.Flags.UserPresent() {
return nil, errors.NewModuleError("auth", "CompleteAuthentication",
fmt.Errorf("user presence flag not set"))
}
// Verify user verification if required
if challenge.UserVerification == "required" && !authData.Flags.UserVerified() {
return nil, errors.NewModuleError("auth", "CompleteAuthentication",
fmt.Errorf("user verification required but not performed"))
}
// Get credential from storage (would normally query blockchain)
// For now, we'll need the credential to be provided or fetched
credential, err := w.getStoredCredential(ctx, credentialID)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "credential not found")
}
// Verify counter progression (prevent replay attacks)
if authData.Counter > 0 && authData.Counter <= credential.Counter {
return nil, errors.NewModuleError("auth", "CompleteAuthentication",
fmt.Errorf("counter did not increase: possible replay attack"))
}
// Construct signature base
clientDataHash := sha256.Sum256(response.ClientDataJSON)
signatureBase := append(response.AuthenticatorData, clientDataHash[:]...)
// Parse and verify signature
publicKey, err := webauthncose.ParsePublicKey(credential.PublicKey)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "public key parsing failed")
}
// Verify signature based on key type
var signatureValid bool
switch pk := publicKey.(type) {
case *webauthncose.EC2PublicKeyData:
signatureValid, err = pk.Verify(signatureBase, response.Signature)
case *webauthncose.RSAPublicKeyData:
signatureValid, err = pk.Verify(signatureBase, response.Signature)
case *webauthncose.OKPPublicKeyData:
signatureValid, err = pk.Verify(signatureBase, response.Signature)
default:
return nil, errors.NewModuleError("auth", "CompleteAuthentication",
fmt.Errorf("unsupported public key type"))
}
if err != nil || !signatureValid {
return nil, errors.NewModuleError("auth", "CompleteAuthentication",
fmt.Errorf("signature verification failed"))
}
// Update credential counter
credential.Counter = authData.Counter
credential.LastUsed = time.Now().UTC().Format(time.RFC3339)
// Create authentication result
result := &AuthenticationResult{
Success: true,
Verified: true,
Credential: credential,
Counter: authData.Counter,
UserHandle: response.UserHandle,
}
return result, nil
}
// getStoredCredential retrieves a credential from storage (placeholder).
func (w *webAuthnClient) getStoredCredential(ctx context.Context, credentialID string) (*WebAuthnCredential, error) {
// This would typically query the blockchain for the credential
// For now, return an error indicating implementation is needed
return nil, fmt.Errorf("credential storage not yet implemented")
}
// ListCredentials lists WebAuthn credentials for a user.
func (w *webAuthnClient) ListCredentials(ctx context.Context, userID string) ([]*WebAuthnCredential, error) {
// Validate user ID
if userID == "" {
return nil, errors.NewModuleError("auth", "ListCredentials",
fmt.Errorf("user ID cannot be empty"))
}
// Query DID module for user's credentials
// This would typically use the DID module's query client
credentials, err := w.queryUserCredentials(ctx, userID)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to query credentials")
}
// Filter out revoked credentials
activeCredentials := make([]*WebAuthnCredential, 0)
for _, cred := range credentials {
// Check if credential is active (not using Status field)
if cred != nil {
activeCredentials = append(activeCredentials, cred)
}
}
return activeCredentials, nil
}
// GetCredential retrieves a specific WebAuthn credential.
func (w *webAuthnClient) GetCredential(ctx context.Context, credentialID string) (*WebAuthnCredential, error) {
// Validate credential ID
if credentialID == "" {
return nil, errors.NewModuleError("auth", "GetCredential",
fmt.Errorf("credential ID cannot be empty"))
}
// Decode credential ID if base64 encoded
credID, err := base64.URLEncoding.DecodeString(credentialID)
if err != nil {
// Try using raw credential ID
credID = []byte(credentialID)
}
// Query DID module for specific credential
credential, err := w.queryCredentialByID(ctx, string(credID))
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "credential not found")
}
// Check if credential is valid
if credential == nil {
return nil, errors.NewModuleError("auth", "GetCredential",
fmt.Errorf("credential not found"))
}
return credential, nil
}
// UpdateCredential updates a WebAuthn credential.
func (w *webAuthnClient) UpdateCredential(ctx context.Context, credentialID string, opts *UpdateCredentialOptions) (*WebAuthnCredential, error) {
// Get existing credential
credential, err := w.GetCredential(ctx, credentialID)
if err != nil {
return nil, err
}
// Update metadata
if opts.Metadata != nil {
credential.Metadata = opts.Metadata
}
credential.LastUsed = time.Now().UTC().Format(time.RFC3339)
// Submit update transaction to chain
// This would create a MsgUpdateWebAuthnCredential
if err := w.submitCredentialUpdate(ctx, credential); err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to update credential")
}
return credential, nil
}
// RevokeCredential revokes a WebAuthn credential.
func (w *webAuthnClient) RevokeCredential(ctx context.Context, credentialID string) error {
// Get existing credential
credential, err := w.GetCredential(ctx, credentialID)
if err != nil {
return err
}
// Check if credential exists
if credential == nil {
return errors.NewModuleError("auth", "RevokeCredential",
fmt.Errorf("credential not found"))
}
// Submit revocation transaction to chain
// This would create a MsgRevokeWebAuthnCredential
if err := w.submitCredentialRevocation(ctx, credentialID); err != nil {
return errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to revoke credential")
}
return nil
}
// Helper methods for DID module interaction (placeholders)
func (w *webAuthnClient) queryUserCredentials(ctx context.Context, userID string) ([]*WebAuthnCredential, error) {
// Placeholder: would query DID module
return []*WebAuthnCredential{}, nil
}
func (w *webAuthnClient) queryCredentialByID(ctx context.Context, credentialID string) (*WebAuthnCredential, error) {
// Placeholder: would query DID module
return nil, fmt.Errorf("DID module query not yet implemented")
}
func (w *webAuthnClient) submitCredentialUpdate(ctx context.Context, credential *WebAuthnCredential) error {
// Placeholder: would submit transaction to DID module
return nil
}
func (w *webAuthnClient) submitCredentialRevocation(ctx context.Context, credentialID string) error {
// Placeholder: would submit transaction to DID module
return nil
}
// RegisterWithDID registers a WebAuthn credential with a DID.
func (w *webAuthnClient) RegisterWithDID(ctx context.Context, did string, opts *DIDRegistrationOptions) (*DIDWebAuthnBinding, error) {
// Begin registration challenge
regChallenge, err := w.BeginRegistration(ctx, opts.CredentialOptions)
if err != nil {
return nil, err
}
// Store challenge for later verification
// In production, this would be stored in a session or cache
binding := &DIDWebAuthnBinding{
DID: did,
CredentialID: base64.URLEncoding.EncodeToString(regChallenge.Challenge),
VerificationMethod: opts.VerificationMethod,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
return binding, nil
}
// AuthenticateWithDID authenticates using WebAuthn and associates with a DID.
func (w *webAuthnClient) AuthenticateWithDID(ctx context.Context, did string, opts *DIDAuthenticationOptions) (*DIDAuthenticationResult, error) {
// Validate DID format
if did == "" {
return nil, errors.NewModuleError("auth", "AuthenticateWithDID",
fmt.Errorf("DID cannot be empty"))
}
// Resolve DID to find WebAuthn verification methods
didDocument, err := w.resolveDID(ctx, did)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to resolve DID")
}
// Extract WebAuthn credentials from DID document
webauthnCredentials := w.extractWebAuthnCredentials(didDocument)
if len(webauthnCredentials) == 0 {
return nil, errors.NewModuleError("auth", "AuthenticateWithDID",
fmt.Errorf("no WebAuthn credentials found in DID document"))
}
// Create authentication options with allowed credentials
authOpts := &AuthenticationOptions{
UserVerification: "preferred",
Timeout: 60000,
AllowedCredentials: make([]*CredentialDescriptor, 0),
}
for _, cred := range webauthnCredentials {
credIDBytes, _ := base64.URLEncoding.DecodeString(cred.ID)
authOpts.AllowedCredentials = append(authOpts.AllowedCredentials, &CredentialDescriptor{
Type: "public-key",
ID: credIDBytes,
Transports: cred.Transports,
})
}
// Begin authentication challenge
challenge, err := w.BeginAuthentication(ctx, authOpts)
if err != nil {
return nil, err
}
// Store challenge for later verification (in production, use session/cache)
w.pendingChallenges[did] = challenge
// Create DID authentication result
result := &DIDAuthenticationResult{
Success: true,
DID: did,
AuthenticationResult: nil, // Will be populated after authentication completion
WalletIdentity: nil, // Will be populated if wallet binding exists
}
return result, nil
}
// resolveDID resolves a DID document (placeholder).
func (w *webAuthnClient) resolveDID(ctx context.Context, did string) (*DIDDocument, error) {
// Placeholder: would query DID resolver
return &DIDDocument{
ID: did,
}, nil
}
// extractWebAuthnCredentials extracts WebAuthn credentials from DID document.
func (w *webAuthnClient) extractWebAuthnCredentials(doc *DIDDocument) []*WebAuthnCredential {
credentials := make([]*WebAuthnCredential, 0)
// Extract from verification methods
// This would parse the DID document structure
return credentials
}
// BindToWallet binds a WebAuthn credential to a Decentralized Abstracted Smart Wallet.
func (w *webAuthnClient) BindToWallet(ctx context.Context, credentialID string, keyring keys.KeyringManager) (*WalletBinding, error) {
// Get wallet identity
identity, err := keyring.GetIssuerDID(ctx)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to get wallet identity")
}
// TODO: Implement actual binding logic
// This would store the binding relationship
binding := &WalletBinding{
CredentialID: credentialID,
WalletIdentity: identity,
BindingType: "primary",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
return binding, nil
}
// BindCredential binds a WebAuthn credential to an existing DID.
// This allows using an existing WebAuthn credential with a DID that was created through other means.
func (w *webAuthnClient) BindCredential(ctx context.Context, did string, credential *WebAuthnCredential) error {
// Validate inputs
if did == "" {
return errors.NewModuleError("auth", "BindCredential",
fmt.Errorf("DID cannot be empty"))
}
if credential == nil {
return errors.NewModuleError("auth", "BindCredential",
fmt.Errorf("credential cannot be nil"))
}
if credential.ID == "" {
return errors.NewModuleError("auth", "BindCredential",
fmt.Errorf("credential ID cannot be empty"))
}
if len(credential.PublicKey) == 0 {
return errors.NewModuleError("auth", "BindCredential",
fmt.Errorf("credential public key cannot be empty"))
}
// TODO: In a real implementation, this would:
// 1. Verify the DID exists in the DID registry
// 2. Verify the caller has permission to bind credentials to this DID
// 3. Store the binding in the DID document as a verification method
// 4. Emit an event for the binding creation
// For now, we simulate success
return nil
}
// SignWithWebAuthn signs data using a WebAuthn credential.
func (w *webAuthnClient) SignWithWebAuthn(ctx context.Context, credentialID string, data []byte) (*WebAuthnSignature, error) {
// Validate inputs
if credentialID == "" {
return nil, errors.NewModuleError("auth", "SignWithWebAuthn",
fmt.Errorf("credential ID cannot be empty"))
}
if len(data) == 0 {
return nil, errors.NewModuleError("auth", "SignWithWebAuthn",
fmt.Errorf("data to sign cannot be empty"))
}
// Create challenge from data hash
dataHash := sha256.Sum256(data)
// Create authentication challenge for signing
credIDBytes, _ := base64.URLEncoding.DecodeString(credentialID)
authOpts := &AuthenticationOptions{
UserVerification: "required", // Require user verification for signing
Timeout: 60000,
AllowedCredentials: []*CredentialDescriptor{
{
Type: "public-key",
ID: credIDBytes,
},
},
}
// Begin authentication for signing
challenge, err := w.BeginAuthentication(ctx, authOpts)
if err != nil {
return nil, errors.WrapError(err, errors.ErrWebAuthnFailed, "failed to create signing challenge")
}
// Store the data hash with the challenge for verification
w.pendingSignatures[string(challenge.Challenge)] = dataHash[:]
// Create WebAuthn signature structure
signature := &WebAuthnSignature{
CredentialID: credentialID,
Signature: challenge.Challenge, // Placeholder - would be actual signature
Counter: 0,
AuthenticatorData: []byte{},
ClientDataJSON: []byte{},
}
// In a complete implementation, this would:
// 1. Wait for user to complete WebAuthn assertion
// 2. Verify the assertion response
// 3. Extract the signature from the assertion
// 4. Return the signature suitable for blockchain transaction
return signature, nil
}
// CompleteSignature completes a WebAuthn signature operation.
func (w *webAuthnClient) CompleteSignature(ctx context.Context, challenge []byte, response *AuthenticatorAssertionResponse) (*WebAuthnSignature, error) {
// Get the pending data hash
_, exists := w.pendingSignatures[string(challenge)]
if !exists {
return nil, errors.NewModuleError("auth", "CompleteSignature",
fmt.Errorf("no pending signature for challenge"))
}
// Create authentication challenge structure
authChallenge := &AuthenticationChallenge{
Challenge: challenge,
UserVerification: "required",
RelyingPartyID: w.rpID,
}
// Verify the assertion with credential ID
// Note: In a real implementation, we'd need to determine the credential ID from the response
credentialID := "" // This would be extracted from response or passed as parameter
result, err := w.CompleteAuthentication(ctx, authChallenge, response, credentialID)
if err != nil {
return nil, err
}
// Create final signature
signature := &WebAuthnSignature{
CredentialID: credentialID,
Signature: response.Signature,
Counter: result.Counter,
AuthenticatorData: response.AuthenticatorData,
ClientDataJSON: response.ClientDataJSON,
}
// Clean up pending signature
delete(w.pendingSignatures, string(challenge))
return signature, nil
}
// Utility functions
// GenerateChallenge generates a cryptographically secure challenge.
func GenerateChallenge() ([]byte, error) {
challenge := make([]byte, 32)
_, err := rand.Read(challenge)
return challenge, err
}
// verifyClientData verifies the client data JSON from WebAuthn response.
func verifyClientData(clientDataJSON []byte, challenge []byte, ceremonyType string, expectedOrigin string) (*webauthn.CollectedClientData, error) {
var clientData webauthn.CollectedClientData
if err := json.Unmarshal(clientDataJSON, &clientData); err != nil {
return nil, fmt.Errorf("failed to parse client data JSON: %w", err)
}
// Verify type
if string(clientData.Type) != ceremonyType {
return nil, fmt.Errorf("invalid ceremony type: expected %s, got %s", ceremonyType, clientData.Type)
}
// Verify challenge
challengeB64 := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(challenge)
if clientData.Challenge != challengeB64 {
return nil, fmt.Errorf("challenge mismatch")
}
// Verify origin
if clientData.Origin != expectedOrigin {
return nil, fmt.Errorf("origin mismatch: expected %s, got %s", expectedOrigin, clientData.Origin)
}
return &clientData, nil
}
// parseAttestationObject parses the attestation object from CBOR format.
func parseAttestationObject(attestationObjBytes []byte) (*webauthn.AttestationObject, error) {
var attestationObj webauthn.AttestationObject
// Decode CBOR attestation object
var rawObj map[string]any
if err := webauthncbor.Unmarshal(attestationObjBytes, &rawObj); err != nil {
return nil, fmt.Errorf("failed to decode attestation object: %w", err)
}
// Extract format
if format, ok := rawObj["fmt"].(string); ok {
attestationObj.Format = format
} else {
return nil, fmt.Errorf("missing attestation format")
}
// Extract authenticator data
if authData, ok := rawObj["authData"].([]byte); ok {
attestationObj.RawAuthData = authData
if err := attestationObj.AuthData.Unmarshal(authData); err != nil {
return nil, fmt.Errorf("failed to parse authenticator data: %w", err)
}
} else {
return nil, fmt.Errorf("missing authenticator data")
}
// Extract attestation statement
if attStmt, ok := rawObj["attStmt"].(map[string]any); ok {
attestationObj.AttStatement = attStmt
}
return &attestationObj, nil
}
// verifyAuthenticatorData verifies the authenticator data against the RP ID.
func verifyAuthenticatorData(authData webauthn.AuthenticatorData, rpID string) error {
// Calculate RP ID hash
rpIDHash := sha256.Sum256([]byte(rpID))
// Verify RP ID hash
if !bytes.Equal(authData.RPIDHash[:], rpIDHash[:]) {
return fmt.Errorf("RP ID hash mismatch")
}
// Verify user presence flag
if !authData.Flags.UserPresent() {
return fmt.Errorf("user presence flag not set")
}
// Verify attestation data is present for registration
if !authData.Flags.HasAttestedCredentialData() {
return fmt.Errorf("attestation data flag not set")
}
return nil
}
// parseCOSEPublicKey parses a COSE public key.
func parseCOSEPublicKey(publicKeyBytes []byte) (*webauthncose.PublicKeyData, error) {
parsed, err := webauthncose.ParsePublicKey(publicKeyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse COSE public key: %w", err)
}
// Type assert to get the base public key data
switch pk := parsed.(type) {
case *webauthncose.EC2PublicKeyData:
return &pk.PublicKeyData, nil
case *webauthncose.RSAPublicKeyData:
return &pk.PublicKeyData, nil
case *webauthncose.OKPPublicKeyData:
return &pk.PublicKeyData, nil
default:
return nil, fmt.Errorf("unsupported public key type")
}
}
// verifyAttestation verifies the attestation statement.
func verifyAttestation(attestationObj *webauthn.AttestationObject, clientDataHash []byte) error {
// For now, we'll implement basic verification
// Full attestation verification would use the registered attestation format handlers
switch attestationObj.Format {
case "none":
// No attestation to verify
return nil
case "packed":
// Verify packed attestation format
return verifyPackedAttestation(attestationObj, clientDataHash)
case "fido-u2f":
// Verify FIDO U2F attestation format
return fmt.Errorf("fido-u2f attestation not yet implemented")
default:
// Unknown attestation format - could be valid but unsupported
return fmt.Errorf("unsupported attestation format: %s", attestationObj.Format)
}
}
// verifyPackedAttestation verifies packed attestation format.
func verifyPackedAttestation(attestationObj *webauthn.AttestationObject, clientDataHash []byte) error {
// Get algorithm from attestation statement
alg, ok := attestationObj.AttStatement["alg"].(int64)
if !ok {
return fmt.Errorf("missing algorithm in attestation statement")
}
// Get signature from attestation statement
sig, ok := attestationObj.AttStatement["sig"].([]byte)
if !ok {
return fmt.Errorf("missing signature in attestation statement")
}
// Construct verification data (authenticatorData || clientDataHash)
verificationData := append(attestationObj.RawAuthData, clientDataHash...)
// Check if self-attestation (no x5c)
if _, hasX5c := attestationObj.AttStatement["x5c"]; !hasX5c {
// Self-attestation: verify with credential public key
if len(attestationObj.AuthData.AttData.CredentialID) == 0 {
return fmt.Errorf("missing attestation data for self-attestation")
}
// Parse public key and verify signature
publicKey, err := webauthncose.ParsePublicKey(attestationObj.AuthData.AttData.CredentialPublicKey)
if err != nil {
return fmt.Errorf("failed to parse public key for verification: %w", err)
}
// Verify signature based on key type
switch pk := publicKey.(type) {
case *webauthncose.EC2PublicKeyData:
if pk.Algorithm != alg {
return fmt.Errorf("algorithm mismatch")
}
valid, err := pk.Verify(verificationData, sig)
if err != nil || !valid {
return fmt.Errorf("signature verification failed: %w", err)
}
case *webauthncose.RSAPublicKeyData:
if pk.Algorithm != alg {
return fmt.Errorf("algorithm mismatch")
}
valid, err := pk.Verify(verificationData, sig)
if err != nil || !valid {
return fmt.Errorf("signature verification failed: %w", err)
}
default:
return fmt.Errorf("unsupported key type for self-attestation")
}
} else {
// Full attestation with certificate chain
// This would require parsing x5c certificate chain and verifying
// For now, we'll accept it but log that full verification is pending
return nil // Certificate chain verification not yet implemented
}
return nil
}
// EncodeChallenge encodes a challenge as base64url.
func EncodeChallenge(challenge []byte) string {
return base64.URLEncoding.EncodeToString(challenge)
}
// DecodeChallenge decodes a base64url challenge.
func DecodeChallenge(encoded string) ([]byte, error) {
return base64.URLEncoding.DecodeString(encoded)
}
// CreateDefaultRelyingParty creates a default relying party configuration.
func CreateDefaultRelyingParty(domain string) *RelyingParty {
return &RelyingParty{
ID: domain,
Name: fmt.Sprintf("Sonr (%s)", domain),
}
}
// ValidateCredentialID validates a credential ID format.
func ValidateCredentialID(credentialID string) error {
if len(credentialID) == 0 {
return fmt.Errorf("credential ID cannot be empty")
}
// Decode to ensure it's valid base64
_, err := base64.URLEncoding.DecodeString(credentialID)
if err != nil {
return fmt.Errorf("invalid credential ID format: %w", err)
}
return nil
}