Files
common/webauthn/sonr_services.go

311 lines
8.6 KiB
Go

// Package webauthn provides Sonr-specific WebAuthn service binding functionality
// that integrates with the x/svc module for domain-verified WebAuthn credentials.
//
// This package contains service-related WebAuthn operations that enable binding
// WebAuthn credentials to verified domains for capability-based access control.
package webauthn
import (
"fmt"
"net/url"
"strings"
)
// ServiceBinding represents a binding between a WebAuthn credential and a verified service domain.
type ServiceBinding struct {
// CredentialID is the WebAuthn credential identifier
CredentialID string
// Domain is the verified domain this credential is bound to
Domain string
// ServiceID is the unique identifier for the registered service
ServiceID string
// Permissions are the specific permissions granted to this credential
Permissions []string
// Origin is the WebAuthn origin for this service binding
Origin string
// CreatedAt timestamp when the binding was created
CreatedAt int64
// ExpiresAt timestamp when the binding expires (optional, 0 = no expiry)
ExpiresAt int64
}
// ValidateServiceBinding validates a WebAuthn credential for service binding.
// This ensures that the credential is legitimate and the service domain is verified.
func ValidateServiceBinding(
credential WebAuthnCredential,
domain string,
permissions []string,
) error {
if credential == nil {
return fmt.Errorf("credential cannot be nil")
}
if domain == "" {
return fmt.Errorf("domain cannot be empty")
}
// Validate credential structure first
if err := ValidateStructure(credential); err != nil {
return fmt.Errorf("credential validation failed: %w", err)
}
// Validate domain format
if err := validateServiceDomain(domain); err != nil {
return fmt.Errorf("invalid service domain: %w", err)
}
// Validate permissions
if err := validateServicePermissions(permissions); err != nil {
return fmt.Errorf("invalid service permissions: %w", err)
}
// Validate origin matches domain
if credential.GetOrigin() != "" {
expectedOrigin := fmt.Sprintf("https://%s", domain)
if credential.GetOrigin() != expectedOrigin {
return fmt.Errorf("credential origin %s does not match expected service origin %s",
credential.GetOrigin(), expectedOrigin)
}
}
return nil
}
// validateServiceDomain validates that a domain meets service binding requirements.
func validateServiceDomain(domain string) error {
if domain == "" {
return fmt.Errorf("domain cannot be empty")
}
// Basic domain validation - should not include protocol or path
if strings.Contains(domain, "://") {
return fmt.Errorf("domain should not include protocol (https://)")
}
if strings.Contains(domain, "/") {
return fmt.Errorf("domain should not include path")
}
// Check domain format using URL parsing
testURL := "https://" + domain
parsedURL, err := url.Parse(testURL)
if err != nil {
return fmt.Errorf("invalid domain format: %w", err)
}
if parsedURL.Hostname() != domain {
return fmt.Errorf("invalid domain format")
}
return nil
}
// validateServicePermissions validates the permissions granted to a service-bound credential.
func validateServicePermissions(permissions []string) error {
if len(permissions) == 0 {
return fmt.Errorf("at least one permission must be specified")
}
validPermissions := map[string]bool{
"read": true,
"write": true,
"execute": true,
"admin": true,
"delegate": true,
}
for _, perm := range permissions {
if !validPermissions[perm] {
return fmt.Errorf("invalid permission: %s", perm)
}
}
return nil
}
// GenerateServiceOrigin generates the expected WebAuthn origin for a service domain.
func GenerateServiceOrigin(domain string) (string, error) {
if err := validateServiceDomain(domain); err != nil {
return "", fmt.Errorf("invalid domain: %w", err)
}
return fmt.Sprintf("https://%s", domain), nil
}
// ValidateCredentialForDomain validates that a WebAuthn credential is valid for a specific domain.
// This includes checking the origin and ensuring the credential can be bound to the domain.
func ValidateCredentialForDomain(
credential WebAuthnCredential,
domain string,
challengeToken string,
) error {
if credential == nil {
return fmt.Errorf("credential cannot be nil")
}
// Generate expected origin for the domain
expectedOrigin, err := GenerateServiceOrigin(domain)
if err != nil {
return fmt.Errorf("failed to generate origin: %w", err)
}
// Validate the credential with domain-specific requirements
if err := ValidateForGaslessRegistration(credential, challengeToken, expectedOrigin); err != nil {
return fmt.Errorf("credential validation failed for domain %s: %w", domain, err)
}
return nil
}
// CreateServiceBinding creates a new service binding for a WebAuthn credential.
func CreateServiceBinding(
credentialID string,
domain string,
serviceID string,
permissions []string,
createdAt int64,
) (*ServiceBinding, error) {
if credentialID == "" {
return nil, fmt.Errorf("credential ID cannot be empty")
}
if err := validateServiceDomain(domain); err != nil {
return nil, fmt.Errorf("invalid domain: %w", err)
}
if serviceID == "" {
return nil, fmt.Errorf("service ID cannot be empty")
}
if err := validateServicePermissions(permissions); err != nil {
return nil, fmt.Errorf("invalid permissions: %w", err)
}
origin, err := GenerateServiceOrigin(domain)
if err != nil {
return nil, fmt.Errorf("failed to generate origin: %w", err)
}
return &ServiceBinding{
CredentialID: credentialID,
Domain: domain,
ServiceID: serviceID,
Permissions: permissions,
Origin: origin,
CreatedAt: createdAt,
ExpiresAt: 0, // No expiration by default
}, nil
}
// ValidateServiceBindingPermission checks if a service binding has a specific permission.
func ValidateServiceBindingPermission(binding *ServiceBinding, requiredPermission string) error {
if binding == nil {
return fmt.Errorf("service binding cannot be nil")
}
// Check if the binding has the required permission
for _, perm := range binding.Permissions {
if perm == requiredPermission || perm == "admin" {
return nil // Permission granted
}
}
return fmt.Errorf("permission denied: %s not granted to credential %s for domain %s",
requiredPermission, binding.CredentialID, binding.Domain)
}
// IsServiceBindingExpired checks if a service binding has expired.
func IsServiceBindingExpired(binding *ServiceBinding, currentTime int64) bool {
if binding == nil {
return true
}
// If ExpiresAt is 0, the binding never expires
if binding.ExpiresAt == 0 {
return false
}
return currentTime >= binding.ExpiresAt
}
// ValidateServiceBindingAccess validates that a credential can access a service with specific permissions.
func ValidateServiceBindingAccess(
binding *ServiceBinding,
requiredPermission string,
currentTime int64,
) error {
if binding == nil {
return fmt.Errorf("no service binding found")
}
// Check if binding is expired
if IsServiceBindingExpired(binding, currentTime) {
return fmt.Errorf("service binding expired at %d", binding.ExpiresAt)
}
// Check permission
if err := ValidateServiceBindingPermission(binding, requiredPermission); err != nil {
return err
}
return nil
}
// GenerateServiceBindingID generates a unique ID for a service binding.
func GenerateServiceBindingID(credentialID, domain, serviceID string) string {
return fmt.Sprintf("%s:%s:%s", credentialID, domain, serviceID)
}
// ValidateDomainVerificationForBinding ensures that a domain is properly verified
// before allowing WebAuthn credential binding.
func ValidateDomainVerificationForBinding(
domain string,
verificationStatus string,
verifiedAt int64,
currentTime int64,
) error {
if domain == "" {
return fmt.Errorf("domain cannot be empty")
}
// Check verification status
if verificationStatus != "DOMAIN_VERIFICATION_STATUS_VERIFIED" {
return fmt.Errorf("domain %s is not verified (status: %s)", domain, verificationStatus)
}
// Ensure verification is not too old (e.g., within 30 days)
const maxVerificationAge = 30 * 24 * 60 * 60 // 30 days in seconds
if currentTime-verifiedAt > maxVerificationAge {
return fmt.Errorf(
"domain verification is too old (verified %d seconds ago)",
currentTime-verifiedAt,
)
}
return nil
}
// ExtractDomainFromOrigin extracts the domain from a WebAuthn origin.
func ExtractDomainFromOrigin(origin string) (string, error) {
if origin == "" {
return "", fmt.Errorf("origin cannot be empty")
}
parsedURL, err := url.Parse(origin)
if err != nil {
return "", fmt.Errorf("invalid origin format: %w", err)
}
if parsedURL.Scheme != "https" {
return "", fmt.Errorf("origin must use HTTPS")
}
return parsedURL.Hostname(), nil
}