Files
next.orly.dev/pkg/protocol/directory/validation.go
mleku e56bf76257
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
Add NIP-11 relay synchronization and group management features
- Introduced a new `sync` package for managing NIP-11 relay information and relay group configurations.
- Implemented a cache for NIP-11 documents, allowing retrieval of relay public keys and authoritative configurations.
- Enhanced the sync manager to update peer lists based on authoritative configurations from relay group events.
- Updated event handling to incorporate policy checks during event imports, ensuring compliance with relay rules.
- Refactored various components to utilize the new `sha256-simd` package for improved performance.
- Added comprehensive tests to validate the new synchronization and group management functionalities.
- Bumped version to v0.24.1 to reflect these changes.
2025-11-03 18:17:15 +00:00

383 lines
11 KiB
Go

package directory
import (
"github.com/minio/sha256-simd"
"encoding/hex"
"net/url"
"regexp"
"strings"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/crypto/ec/schnorr"
"next.orly.dev/pkg/crypto/ec/secp256k1"
"next.orly.dev/pkg/encoders/bech32encoding"
"next.orly.dev/pkg/encoders/event"
)
// Validation constants
const (
MaxKeyDelegations = 512
KeyExpirationDays = 30
MinNonceSize = 16 // bytes
MaxContentLength = 65536 // bytes
)
// Regular expressions for validation
var (
hexKeyRegex = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
npubRegex = regexp.MustCompile(`^npub1[0-9a-z]+$`)
wsURLRegex = regexp.MustCompile(`^wss?://[a-zA-Z0-9.-]+(?::[0-9]+)?(?:/.*)?$`)
groupTagNameRegex = regexp.MustCompile(`^[a-zA-Z0-9._~-]+$`) // RFC 3986 URL-safe characters
)
// ValidateGroupTagName validates that a group tag name is URL-safe (RFC 3986).
func ValidateGroupTagName(name string) (err error) {
if len(name) < 1 {
return errorf.E("group tag name cannot be empty")
}
if len(name) > 255 {
return errorf.E("group tag name cannot exceed 255 characters")
}
// Check for reserved prefixes
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
return errorf.E("group tag names starting with '.' or '_' are reserved for system use")
}
// Validate URL-safe character set
if !groupTagNameRegex.MatchString(name) {
return errorf.E("group tag name must contain only URL-safe characters (a-z, A-Z, 0-9, -, ., _, ~)")
}
return nil
}
// ValidateHexKey validates that a string is a valid 64-character hex key.
func ValidateHexKey(key string) (err error) {
if !hexKeyRegex.MatchString(key) {
return errorf.E("invalid hex key format: must be 64 hex characters")
}
return nil
}
// ValidateNPub validates that a string is a valid npub-encoded public key.
func ValidateNPub(npub string) (err error) {
if !npubRegex.MatchString(npub) {
return errorf.E("invalid npub format")
}
// Try to decode to verify it's valid
if _, err = bech32encoding.NpubToBytes(npub); chk.E(err) {
return errorf.E("invalid npub encoding: %w", err)
}
return nil
}
// ValidateWebSocketURL validates that a string is a valid WebSocket URL.
func ValidateWebSocketURL(wsURL string) (err error) {
if !wsURLRegex.MatchString(wsURL) {
return errorf.E("invalid WebSocket URL format")
}
// Parse URL for additional validation
var u *url.URL
if u, err = url.Parse(wsURL); chk.E(err) {
return errorf.E("invalid URL: %w", err)
}
if u.Scheme != "ws" && u.Scheme != "wss" {
return errorf.E("URL must use ws:// or wss:// scheme")
}
if u.Host == "" {
return errorf.E("URL must have a host")
}
return nil
}
// ValidateNonce validates that a nonce meets minimum security requirements.
func ValidateNonce(nonce string) (err error) {
if len(nonce) < MinNonceSize*2 { // hex-encoded, so double the byte length
return errorf.E("nonce must be at least %d bytes (%d hex characters)",
MinNonceSize, MinNonceSize*2)
}
// Verify it's valid hex
if _, err = hex.DecodeString(nonce); chk.E(err) {
return errorf.E("nonce must be valid hex: %w", err)
}
return nil
}
// ValidateSignature validates that a signature is properly formatted.
func ValidateSignature(sig string) (err error) {
if len(sig) != 128 { // 64 bytes hex-encoded
return errorf.E("signature must be 64 bytes (128 hex characters)")
}
// Verify it's valid hex
if _, err = hex.DecodeString(sig); chk.E(err) {
return errorf.E("signature must be valid hex: %w", err)
}
return nil
}
// ValidateDerivationPath validates a BIP32 derivation path for this protocol.
func ValidateDerivationPath(path string) (err error) {
// Expected format: m/39103'/1237'/identity'/usage/index
if !strings.HasPrefix(path, "m/39103'/1237'/") {
return errorf.E("derivation path must start with m/39103'/1237'/")
}
parts := strings.Split(path, "/")
if len(parts) != 6 {
return errorf.E("derivation path must have 6 components")
}
// Validate hardened components
if parts[1] != "39103'" || parts[2] != "1237'" {
return errorf.E("invalid hardened components in derivation path")
}
// Identity component should be hardened (end with ')
if !strings.HasSuffix(parts[3], "'") {
return errorf.E("identity component must be hardened")
}
return nil
}
// ValidateEventContent validates that event content is within size limits.
func ValidateEventContent(content []byte) (err error) {
if len(content) > MaxContentLength {
return errorf.E("content exceeds maximum length of %d bytes", MaxContentLength)
}
return nil
}
// ValidateTimestamp validates that a timestamp is reasonable (not too far in past/future).
func ValidateTimestamp(ts int64) (err error) {
now := time.Now().Unix()
// Allow up to 1 hour in the future
if ts > now+3600 {
return errorf.E("timestamp too far in the future")
}
// Allow up to 1 year in the past
if ts < now-31536000 {
return errorf.E("timestamp too far in the past")
}
return nil
}
// VerifyIdentityTagSignature verifies the signature in an identity tag.
func VerifyIdentityTagSignature(
identityTag *IdentityTag,
delegatePubkey []byte,
) (valid bool, err error) {
if identityTag == nil {
return false, errorf.E("identity tag cannot be nil")
}
// Decode npub to get identity public key
var identityPubkey []byte
if identityPubkey, err = bech32encoding.NpubToBytes(identityTag.NPubIdentity); chk.E(err) {
return false, errorf.E("failed to decode npub: %w", err)
}
// Decode nonce and signature
var nonce, signature []byte
if nonce, err = hex.DecodeString(identityTag.Nonce); chk.E(err) {
return false, errorf.E("invalid nonce hex: %w", err)
}
if signature, err = hex.DecodeString(identityTag.Signature); chk.E(err) {
return false, errorf.E("invalid signature hex: %w", err)
}
// Create message to verify: nonce + delegate_pubkey_hex + identity_pubkey_hex
message := make([]byte, 0, len(nonce)+64+64)
message = append(message, nonce...)
message = append(message, []byte(hex.EncodeToString(delegatePubkey))...)
message = append(message, []byte(hex.EncodeToString(identityPubkey))...)
// Hash the message
hash := sha256.Sum256(message)
// Parse signature and verify
var sig *schnorr.Signature
if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
return false, errorf.E("failed to parse signature: %w", err)
}
// Parse public key
var pubKey *secp256k1.PublicKey
if pubKey, err = schnorr.ParsePubKey(identityPubkey); chk.E(err) {
return false, errorf.E("failed to parse public key: %w", err)
}
return sig.Verify(hash[:], pubKey), nil
}
// ValidateEventKindForReplication validates that an event kind is appropriate
// for replication in the directory consensus protocol.
func ValidateEventKindForReplication(kind uint16) (err error) {
// Directory events are always valid
if IsDirectoryEventKind(kind) {
return nil
}
// Protocol events (39100-39105) should not be replicated as regular events
if kind >= 39100 && kind <= 39105 {
return errorf.E("protocol events should not be replicated as directory events")
}
// Ephemeral events (20000-29999) should not be stored
if kind >= 20000 && kind <= 29999 {
return errorf.E("ephemeral events should not be replicated")
}
return nil
}
// ValidateRelayIdentityBinding verifies that a relay identity announcement
// is properly bound to its network address through NIP-11 signature verification.
func ValidateRelayIdentityBinding(
announcement *RelayIdentityAnnouncement,
nip11Pubkey, nip11Nonce, nip11Sig, relayAddress string,
) (valid bool, err error) {
if announcement == nil {
return false, errorf.E("announcement cannot be nil")
}
// Verify the announcement event pubkey matches the NIP-11 pubkey
announcementPubkeyHex := hex.EncodeToString(announcement.Event.Pubkey)
if announcementPubkeyHex != nip11Pubkey {
return false, errorf.E("announcement pubkey does not match NIP-11 pubkey")
}
// Verify NIP-11 signature format
if err = ValidateHexKey(nip11Pubkey); chk.E(err) {
return false, errorf.E("invalid NIP-11 pubkey: %w", err)
}
if err = ValidateNonce(nip11Nonce); chk.E(err) {
return false, errorf.E("invalid NIP-11 nonce: %w", err)
}
if err = ValidateSignature(nip11Sig); chk.E(err) {
return false, errorf.E("invalid NIP-11 signature: %w", err)
}
// Decode components
var pubkey, signature []byte
if pubkey, err = hex.DecodeString(nip11Pubkey); chk.E(err) {
return false, errorf.E("failed to decode NIP-11 pubkey: %w", err)
}
if signature, err = hex.DecodeString(nip11Sig); chk.E(err) {
return false, errorf.E("failed to decode NIP-11 signature: %w", err)
}
// Create message: pubkey + nonce + relay_address
message := nip11Pubkey + nip11Nonce + relayAddress
hash := sha256.Sum256([]byte(message))
// Parse signature and verify
var sig *schnorr.Signature
if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
return false, errorf.E("failed to parse signature: %w", err)
}
// Parse public key
var pubKey *secp256k1.PublicKey
if pubKey, err = schnorr.ParsePubKey(pubkey); chk.E(err) {
return false, errorf.E("failed to parse public key: %w", err)
}
return sig.Verify(hash[:], pubKey), nil
}
// ValidateConsortiumEvent performs comprehensive validation of any consortium
// protocol event, including signature verification and protocol-specific checks.
func ValidateConsortiumEvent(ev *event.E) (err error) {
if ev == nil {
return errorf.E("event cannot be nil")
}
// Verify basic event signature
if _, err = ev.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate timestamp
if err = ValidateTimestamp(ev.CreatedAt); chk.E(err) {
return errorf.E("invalid timestamp: %w", err)
}
// Validate content size
if err = ValidateEventContent(ev.Content); chk.E(err) {
return errorf.E("invalid content: %w", err)
}
// Protocol-specific validation based on event kind
switch ev.Kind {
case RelayIdentityAnnouncementKind.K:
var ria *RelayIdentityAnnouncement
if ria, err = ParseRelayIdentityAnnouncement(ev); chk.E(err) {
return errorf.E("failed to parse relay identity announcement: %w", err)
}
return ria.Validate()
case TrustActKind.K:
var ta *TrustAct
if ta, err = ParseTrustAct(ev); chk.E(err) {
return errorf.E("failed to parse trust act: %w", err)
}
return ta.Validate()
case GroupTagActKind.K:
var gta *GroupTagAct
if gta, err = ParseGroupTagAct(ev); chk.E(err) {
return errorf.E("failed to parse group tag act: %w", err)
}
return gta.Validate()
case PublicKeyAdvertisementKind.K:
var pka *PublicKeyAdvertisement
if pka, err = ParsePublicKeyAdvertisement(ev); chk.E(err) {
return errorf.E("failed to parse public key advertisement: %w", err)
}
return pka.Validate()
case DirectoryEventReplicationRequestKind.K:
var derr *DirectoryEventReplicationRequest
if derr, err = ParseDirectoryEventReplicationRequest(ev); chk.E(err) {
return errorf.E("failed to parse replication request: %w", err)
}
return derr.Validate()
case DirectoryEventReplicationResponseKind.K:
var derr *DirectoryEventReplicationResponse
if derr, err = ParseDirectoryEventReplicationResponse(ev); chk.E(err) {
return errorf.E("failed to parse replication response: %w", err)
}
return derr.Validate()
default:
return errorf.E("unknown consortium event kind: %d", ev.Kind)
}
}
// IsConsortiumEvent returns true if the event is a consortium protocol event.
func IsConsortiumEvent(ev *event.E) bool {
if ev == nil {
return false
}
return ev.Kind >= 39100 && ev.Kind <= 39105
}