mirror of
https://github.com/sonr-io/common.git
synced 2026-01-12 04:09:13 +00:00
311 lines
8.6 KiB
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
|
|
}
|