mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
* 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>
1103 lines
40 KiB
Go
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
|
|
}
|