Files
sonr/x/svc/keeper/domain_verification.go
T

300 lines
9.2 KiB
Go
Raw Normal View History

2025-10-03 14:45:52 -04:00
package keeper
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net"
"strings"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1 "github.com/sonr-io/sonr/api/svc/v1"
)
// Domain verification constants
const (
// VerificationPrefix is the prefix for DNS TXT records
VerificationPrefix = "sonr-verification="
// TokenLength is the length of the verification token in bytes
TokenLength = 32
// VerificationExpiryHours is how long a verification token is valid
VerificationExpiryHours = 24
)
// InitiateDomainVerification creates a new domain verification request
func (k Keeper) InitiateDomainVerification(
ctx context.Context,
domain, owner string,
) (*v1.DomainVerification, error) {
// Validate domain format
if err := k.validateDomainFormat(domain); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid domain format: %v", err)
}
// Check if domain verification already exists and is not expired
existing, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err == nil {
// Domain verification exists, check if it's still valid
if k.isDomainVerificationValid(existing) {
return existing, status.Errorf(
codes.AlreadyExists,
"domain verification already exists and is valid",
)
}
// Expired verification exists, we'll update it
}
// Generate a new verification token
token, err := k.generateVerificationToken()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate verification token: %v", err)
}
// Create new domain verification record
now := time.Now().Unix()
verification := &v1.DomainVerification{
Domain: domain,
Owner: owner,
VerificationToken: token,
Status: v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_PENDING,
ExpiresAt: now + (VerificationExpiryHours * 3600), // 24 hours from now
VerifiedAt: 0,
}
// Save or update the verification record
if existing != nil {
// Update existing record
verification.Domain = existing.Domain // Ensure primary key consistency
err = k.OrmDB.DomainVerificationTable().Update(ctx, verification)
} else {
// Insert new record
err = k.OrmDB.DomainVerificationTable().Insert(ctx, verification)
}
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to save domain verification: %v", err)
}
return verification, nil
}
// VerifyDomainOwnership validates domain ownership by checking DNS TXT records
func (k Keeper) VerifyDomainOwnership(
ctx context.Context,
domain string,
) (*v1.DomainVerification, error) {
// Get the domain verification record
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return nil, status.Errorf(codes.NotFound, "domain verification not found: %v", err)
}
// Check if verification has expired
if k.isDomainVerificationExpired(verification) {
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_EXPIRED
k.OrmDB.DomainVerificationTable().Update(ctx, verification)
return verification, status.Errorf(
codes.DeadlineExceeded,
"domain verification has expired",
)
}
// Check if already verified
if verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
return verification, nil
}
// Perform DNS TXT record lookup
verified, err := k.checkDNSTXTRecord(domain, verification.VerificationToken)
if err != nil {
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED
k.OrmDB.DomainVerificationTable().Update(ctx, verification)
return verification, status.Errorf(
codes.FailedPrecondition,
"DNS verification failed: %v",
err,
)
}
if verified {
// Mark as verified
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
verification.VerifiedAt = time.Now().Unix()
} else {
// Verification record not found
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED
}
// Update the verification record
err = k.OrmDB.DomainVerificationTable().Update(ctx, verification)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update domain verification: %v", err)
}
if !verified {
return verification, status.Errorf(
codes.FailedPrecondition,
"verification record not found in DNS",
)
}
return verification, nil
}
// GetDomainVerification retrieves a domain verification record
func (k Keeper) GetDomainVerification(
ctx context.Context,
domain string,
) (*v1.DomainVerification, error) {
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return nil, status.Errorf(codes.NotFound, "domain verification not found: %v", err)
}
return verification, nil
}
// ListDomainVerificationsByOwner returns all domain verifications for a given owner
func (k Keeper) ListDomainVerificationsByOwner(
ctx context.Context,
owner string,
) ([]*v1.DomainVerification, error) {
ownerKey := v1.DomainVerificationOwnerIndexKey{}.WithOwner(owner)
iter, err := k.OrmDB.DomainVerificationTable().List(ctx, ownerKey)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list domain verifications: %v", err)
}
defer iter.Close()
var verifications []*v1.DomainVerification
for iter.Next() {
verification, err := iter.Value()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to read domain verification: %v", err)
}
verifications = append(verifications, verification)
}
return verifications, nil
}
// IsVerifiedDomain checks if a domain is verified and not expired
func (k Keeper) IsVerifiedDomain(ctx context.Context, domain string) bool {
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return false
}
return verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED &&
!k.isDomainVerificationExpired(verification)
}
// generateVerificationToken creates a cryptographically secure random token
func (k Keeper) generateVerificationToken() (string, error) {
bytes := make([]byte, TokenLength)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// checkDNSTXTRecord performs DNS TXT record lookup and validation
func (k Keeper) checkDNSTXTRecord(domain, expectedToken string) (bool, error) {
// Expected TXT record format: "sonr-verification=<token>"
expectedRecord := VerificationPrefix + expectedToken
// Perform DNS TXT lookup
txtRecords, err := net.LookupTXT(domain)
if err != nil {
return false, fmt.Errorf("DNS lookup failed: %w", err)
}
// Check if any TXT record matches our expected verification record
for _, record := range txtRecords {
if strings.TrimSpace(record) == expectedRecord {
return true, nil
}
}
return false, nil
}
// validateDomainFormat validates that a domain name is properly formatted
func (k Keeper) validateDomainFormat(domain string) error {
if domain == "" {
return fmt.Errorf("domain cannot be empty")
}
// Basic domain validation - check for valid characters and format
if len(domain) > 253 {
return fmt.Errorf("domain name too long")
}
// Check for valid domain format (basic validation)
if !strings.Contains(domain, ".") {
return fmt.Errorf("domain must contain at least one dot")
}
// Check for invalid characters
for _, char := range domain {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '.' || char == '-') {
return fmt.Errorf("domain contains invalid character: %c", char)
}
}
// Domain cannot start or end with a hyphen
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
return fmt.Errorf("domain cannot start or end with hyphen")
}
return nil
}
// isDomainVerificationValid checks if a domain verification is still valid (not expired)
func (k Keeper) isDomainVerificationValid(verification *v1.DomainVerification) bool {
if verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
return true // Verified domains don't expire
}
return !k.isDomainVerificationExpired(verification)
}
// isDomainVerificationExpired checks if a domain verification has expired
func (k Keeper) isDomainVerificationExpired(verification *v1.DomainVerification) bool {
now := time.Now().Unix()
return now > verification.ExpiresAt
}
// GetDNSInstructions returns human-readable instructions for setting up DNS verification
func (k Keeper) GetDNSInstructions(domain, token string) string {
return fmt.Sprintf(
"Add the following TXT record to your DNS configuration for domain '%s':\n\n"+
"Name: %s\n"+
"Type: TXT\n"+
"Value: %s%s\n\n"+
"Note: DNS propagation may take up to 48 hours. You can verify the record using:\n"+
"dig TXT %s",
domain, domain, VerificationPrefix, token, domain,
)
}
// SetDomainVerified is a helper method for testing to mark a domain as verified
func (k Keeper) SetDomainVerified(ctx context.Context, domain string) error {
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
if err != nil {
return fmt.Errorf("domain verification not found: %w", err)
}
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
return k.OrmDB.DomainVerificationTable().Update(ctx, verification)
}