validation.go raw

   1  package directory
   2  
   3  import (
   4  	"github.com/minio/sha256-simd"
   5  	"encoding/hex"
   6  	"net/url"
   7  	"regexp"
   8  	"strings"
   9  	"time"
  10  
  11  	"next.orly.dev/pkg/lol/chk"
  12  	"next.orly.dev/pkg/lol/errorf"
  13  	"next.orly.dev/pkg/nostr/crypto/ec/schnorr"
  14  	"next.orly.dev/pkg/nostr/crypto/ec/secp256k1"
  15  	"next.orly.dev/pkg/nostr/encoders/bech32encoding"
  16  	"next.orly.dev/pkg/nostr/encoders/event"
  17  )
  18  
  19  // Validation constants
  20  const (
  21  	MaxKeyDelegations = 512
  22  	KeyExpirationDays = 30
  23  	MinNonceSize      = 16    // bytes
  24  	MaxContentLength  = 65536 // bytes
  25  )
  26  
  27  // Regular expressions for validation
  28  var (
  29  	hexKeyRegex       = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
  30  	npubRegex         = regexp.MustCompile(`^npub1[0-9a-z]+$`)
  31  	wsURLRegex        = regexp.MustCompile(`^wss?://[a-zA-Z0-9.-]+(?::[0-9]+)?(?:/.*)?$`)
  32  	groupTagNameRegex = regexp.MustCompile(`^[a-zA-Z0-9._~-]+$`) // RFC 3986 URL-safe characters
  33  )
  34  
  35  // ValidateGroupTagName validates that a group tag name is URL-safe (RFC 3986).
  36  func ValidateGroupTagName(name string) (err error) {
  37  	if len(name) < 1 {
  38  		return errorf.E("group tag name cannot be empty")
  39  	}
  40  	if len(name) > 255 {
  41  		return errorf.E("group tag name cannot exceed 255 characters")
  42  	}
  43  
  44  	// Check for reserved prefixes
  45  	if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
  46  		return errorf.E("group tag names starting with '.' or '_' are reserved for system use")
  47  	}
  48  
  49  	// Validate URL-safe character set
  50  	if !groupTagNameRegex.MatchString(name) {
  51  		return errorf.E("group tag name must contain only URL-safe characters (a-z, A-Z, 0-9, -, ., _, ~)")
  52  	}
  53  
  54  	return nil
  55  }
  56  
  57  // ValidateHexKey validates that a string is a valid 64-character hex key.
  58  func ValidateHexKey(key string) (err error) {
  59  	if !hexKeyRegex.MatchString(key) {
  60  		return errorf.E("invalid hex key format: must be 64 hex characters")
  61  	}
  62  	return nil
  63  }
  64  
  65  // ValidateNPub validates that a string is a valid npub-encoded public key.
  66  func ValidateNPub(npub string) (err error) {
  67  	if !npubRegex.MatchString(npub) {
  68  		return errorf.E("invalid npub format")
  69  	}
  70  
  71  	// Try to decode to verify it's valid
  72  	if _, err = bech32encoding.NpubToBytes(npub); chk.E(err) {
  73  		return errorf.E("invalid npub encoding: %w", err)
  74  	}
  75  
  76  	return nil
  77  }
  78  
  79  // ValidateWebSocketURL validates that a string is a valid WebSocket URL.
  80  func ValidateWebSocketURL(wsURL string) (err error) {
  81  	if !wsURLRegex.MatchString(wsURL) {
  82  		return errorf.E("invalid WebSocket URL format")
  83  	}
  84  
  85  	// Parse URL for additional validation
  86  	var u *url.URL
  87  	if u, err = url.Parse(wsURL); chk.E(err) {
  88  		return errorf.E("invalid URL: %w", err)
  89  	}
  90  
  91  	if u.Scheme != "ws" && u.Scheme != "wss" {
  92  		return errorf.E("URL must use ws:// or wss:// scheme")
  93  	}
  94  
  95  	if u.Host == "" {
  96  		return errorf.E("URL must have a host")
  97  	}
  98  
  99  	return nil
 100  }
 101  
 102  // ValidateNonce validates that a nonce meets minimum security requirements.
 103  func ValidateNonce(nonce string) (err error) {
 104  	if len(nonce) < MinNonceSize*2 { // hex-encoded, so double the byte length
 105  		return errorf.E("nonce must be at least %d bytes (%d hex characters)",
 106  			MinNonceSize, MinNonceSize*2)
 107  	}
 108  
 109  	// Verify it's valid hex
 110  	if _, err = hex.DecodeString(nonce); chk.E(err) {
 111  		return errorf.E("nonce must be valid hex: %w", err)
 112  	}
 113  
 114  	return nil
 115  }
 116  
 117  // ValidateSignature validates that a signature is properly formatted.
 118  func ValidateSignature(sig string) (err error) {
 119  	if len(sig) != 128 { // 64 bytes hex-encoded
 120  		return errorf.E("signature must be 64 bytes (128 hex characters)")
 121  	}
 122  
 123  	// Verify it's valid hex
 124  	if _, err = hex.DecodeString(sig); chk.E(err) {
 125  		return errorf.E("signature must be valid hex: %w", err)
 126  	}
 127  
 128  	return nil
 129  }
 130  
 131  // ValidateDerivationPath validates a BIP32 derivation path for this protocol.
 132  func ValidateDerivationPath(path string) (err error) {
 133  	// Expected format: m/39103'/1237'/identity'/usage/index
 134  	if !strings.HasPrefix(path, "m/39103'/1237'/") {
 135  		return errorf.E("derivation path must start with m/39103'/1237'/")
 136  	}
 137  
 138  	parts := strings.Split(path, "/")
 139  	if len(parts) != 6 {
 140  		return errorf.E("derivation path must have 6 components")
 141  	}
 142  
 143  	// Validate hardened components
 144  	if parts[1] != "39103'" || parts[2] != "1237'" {
 145  		return errorf.E("invalid hardened components in derivation path")
 146  	}
 147  
 148  	// Identity component should be hardened (end with ')
 149  	if !strings.HasSuffix(parts[3], "'") {
 150  		return errorf.E("identity component must be hardened")
 151  	}
 152  
 153  	return nil
 154  }
 155  
 156  // ValidateEventContent validates that event content is within size limits.
 157  func ValidateEventContent(content []byte) (err error) {
 158  	if len(content) > MaxContentLength {
 159  		return errorf.E("content exceeds maximum length of %d bytes", MaxContentLength)
 160  	}
 161  	return nil
 162  }
 163  
 164  // ValidateTimestamp validates that a timestamp is reasonable (not too far in past/future).
 165  func ValidateTimestamp(ts int64) (err error) {
 166  	now := time.Now().Unix()
 167  
 168  	// Allow up to 1 hour in the future
 169  	if ts > now+3600 {
 170  		return errorf.E("timestamp too far in the future")
 171  	}
 172  
 173  	// Allow up to 1 year in the past
 174  	if ts < now-31536000 {
 175  		return errorf.E("timestamp too far in the past")
 176  	}
 177  
 178  	return nil
 179  }
 180  
 181  // VerifyIdentityTagSignature verifies the signature in an identity tag.
 182  func VerifyIdentityTagSignature(
 183  	identityTag *IdentityTag,
 184  	delegatePubkey []byte,
 185  ) (valid bool, err error) {
 186  	if identityTag == nil {
 187  		return false, errorf.E("identity tag cannot be nil")
 188  	}
 189  
 190  	// Decode npub to get identity public key
 191  	var identityPubkey []byte
 192  	if identityPubkey, err = bech32encoding.NpubToBytes(identityTag.NPubIdentity); chk.E(err) {
 193  		return false, errorf.E("failed to decode npub: %w", err)
 194  	}
 195  
 196  	// Decode nonce and signature
 197  	var nonce, signature []byte
 198  	if nonce, err = hex.DecodeString(identityTag.Nonce); chk.E(err) {
 199  		return false, errorf.E("invalid nonce hex: %w", err)
 200  	}
 201  	if signature, err = hex.DecodeString(identityTag.Signature); chk.E(err) {
 202  		return false, errorf.E("invalid signature hex: %w", err)
 203  	}
 204  
 205  	// Create message to verify: nonce + delegate_pubkey_hex + identity_pubkey_hex
 206  	message := make([]byte, 0, len(nonce)+64+64)
 207  	message = append(message, nonce...)
 208  	message = append(message, []byte(hex.EncodeToString(delegatePubkey))...)
 209  	message = append(message, []byte(hex.EncodeToString(identityPubkey))...)
 210  
 211  	// Hash the message
 212  	hash := sha256.Sum256(message)
 213  
 214  	// Parse signature and verify
 215  	var sig *schnorr.Signature
 216  	if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
 217  		return false, errorf.E("failed to parse signature: %w", err)
 218  	}
 219  
 220  	// Parse public key
 221  	var pubKey *secp256k1.PublicKey
 222  	if pubKey, err = schnorr.ParsePubKey(identityPubkey); chk.E(err) {
 223  		return false, errorf.E("failed to parse public key: %w", err)
 224  	}
 225  
 226  	return sig.Verify(hash[:], pubKey), nil
 227  }
 228  
 229  // ValidateEventKindForReplication validates that an event kind is appropriate
 230  // for replication in the directory consensus protocol.
 231  func ValidateEventKindForReplication(kind uint16) (err error) {
 232  	// Directory events are always valid
 233  	if IsDirectoryEventKind(kind) {
 234  		return nil
 235  	}
 236  
 237  	// Protocol events (39100-39105) should not be replicated as regular events
 238  	if kind >= 39100 && kind <= 39105 {
 239  		return errorf.E("protocol events should not be replicated as directory events")
 240  	}
 241  
 242  	// Ephemeral events (20000-29999) should not be stored
 243  	if kind >= 20000 && kind <= 29999 {
 244  		return errorf.E("ephemeral events should not be replicated")
 245  	}
 246  
 247  	return nil
 248  }
 249  
 250  // ValidateRelayIdentityBinding verifies that a relay identity announcement
 251  // is properly bound to its network address through NIP-11 signature verification.
 252  func ValidateRelayIdentityBinding(
 253  	announcement *RelayIdentityAnnouncement,
 254  	nip11Pubkey, nip11Nonce, nip11Sig, relayAddress string,
 255  ) (valid bool, err error) {
 256  	if announcement == nil {
 257  		return false, errorf.E("announcement cannot be nil")
 258  	}
 259  
 260  	// Verify the announcement event pubkey matches the NIP-11 pubkey
 261  	announcementPubkeyHex := hex.EncodeToString(announcement.Event.Pubkey)
 262  	if announcementPubkeyHex != nip11Pubkey {
 263  		return false, errorf.E("announcement pubkey does not match NIP-11 pubkey")
 264  	}
 265  
 266  	// Verify NIP-11 signature format
 267  	if err = ValidateHexKey(nip11Pubkey); chk.E(err) {
 268  		return false, errorf.E("invalid NIP-11 pubkey: %w", err)
 269  	}
 270  	if err = ValidateNonce(nip11Nonce); chk.E(err) {
 271  		return false, errorf.E("invalid NIP-11 nonce: %w", err)
 272  	}
 273  	if err = ValidateSignature(nip11Sig); chk.E(err) {
 274  		return false, errorf.E("invalid NIP-11 signature: %w", err)
 275  	}
 276  
 277  	// Decode components
 278  	var pubkey, signature []byte
 279  	if pubkey, err = hex.DecodeString(nip11Pubkey); chk.E(err) {
 280  		return false, errorf.E("failed to decode NIP-11 pubkey: %w", err)
 281  	}
 282  	if signature, err = hex.DecodeString(nip11Sig); chk.E(err) {
 283  		return false, errorf.E("failed to decode NIP-11 signature: %w", err)
 284  	}
 285  
 286  	// Create message: pubkey + nonce + relay_address
 287  	message := nip11Pubkey + nip11Nonce + relayAddress
 288  	hash := sha256.Sum256([]byte(message))
 289  
 290  	// Parse signature and verify
 291  	var sig *schnorr.Signature
 292  	if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
 293  		return false, errorf.E("failed to parse signature: %w", err)
 294  	}
 295  
 296  	// Parse public key
 297  	var pubKey *secp256k1.PublicKey
 298  	if pubKey, err = schnorr.ParsePubKey(pubkey); chk.E(err) {
 299  		return false, errorf.E("failed to parse public key: %w", err)
 300  	}
 301  
 302  	return sig.Verify(hash[:], pubKey), nil
 303  }
 304  
 305  // ValidateConsortiumEvent performs comprehensive validation of any consortium
 306  // protocol event, including signature verification and protocol-specific checks.
 307  func ValidateConsortiumEvent(ev *event.E) (err error) {
 308  	if ev == nil {
 309  		return errorf.E("event cannot be nil")
 310  	}
 311  
 312  	// Verify basic event signature
 313  	if _, err = ev.Verify(); chk.E(err) {
 314  		return errorf.E("invalid event signature: %w", err)
 315  	}
 316  
 317  	// Validate timestamp
 318  	if err = ValidateTimestamp(ev.CreatedAt); chk.E(err) {
 319  		return errorf.E("invalid timestamp: %w", err)
 320  	}
 321  
 322  	// Validate content size
 323  	if err = ValidateEventContent(ev.Content); chk.E(err) {
 324  		return errorf.E("invalid content: %w", err)
 325  	}
 326  
 327  	// Protocol-specific validation based on event kind
 328  	switch ev.Kind {
 329  	case RelayIdentityAnnouncementKind.K:
 330  		var ria *RelayIdentityAnnouncement
 331  		if ria, err = ParseRelayIdentityAnnouncement(ev); chk.E(err) {
 332  			return errorf.E("failed to parse relay identity announcement: %w", err)
 333  		}
 334  		return ria.Validate()
 335  
 336  	case TrustActKind.K:
 337  		var ta *TrustAct
 338  		if ta, err = ParseTrustAct(ev); chk.E(err) {
 339  			return errorf.E("failed to parse trust act: %w", err)
 340  		}
 341  		return ta.Validate()
 342  
 343  	case GroupTagActKind.K:
 344  		var gta *GroupTagAct
 345  		if gta, err = ParseGroupTagAct(ev); chk.E(err) {
 346  			return errorf.E("failed to parse group tag act: %w", err)
 347  		}
 348  		return gta.Validate()
 349  
 350  	case PublicKeyAdvertisementKind.K:
 351  		var pka *PublicKeyAdvertisement
 352  		if pka, err = ParsePublicKeyAdvertisement(ev); chk.E(err) {
 353  			return errorf.E("failed to parse public key advertisement: %w", err)
 354  		}
 355  		return pka.Validate()
 356  
 357  	case DirectoryEventReplicationRequestKind.K:
 358  		var derr *DirectoryEventReplicationRequest
 359  		if derr, err = ParseDirectoryEventReplicationRequest(ev); chk.E(err) {
 360  			return errorf.E("failed to parse replication request: %w", err)
 361  		}
 362  		return derr.Validate()
 363  
 364  	case DirectoryEventReplicationResponseKind.K:
 365  		var derr *DirectoryEventReplicationResponse
 366  		if derr, err = ParseDirectoryEventReplicationResponse(ev); chk.E(err) {
 367  			return errorf.E("failed to parse replication response: %w", err)
 368  		}
 369  		return derr.Validate()
 370  
 371  	default:
 372  		return errorf.E("unknown consortium event kind: %d", ev.Kind)
 373  	}
 374  }
 375  
 376  // IsConsortiumEvent returns true if the event is a consortium protocol event.
 377  func IsConsortiumEvent(ev *event.E) bool {
 378  	if ev == nil {
 379  		return false
 380  	}
 381  	return ev.Kind >= 39100 && ev.Kind <= 39105
 382  }
 383