Refactor NIP-XX Document and Protocol Implementation for Directory Consensus
- Updated the NIP-XX document to clarify terminology, replacing "attestations" with "acts" for consistency. - Enhanced the protocol by introducing new event kinds: Trust Act (Kind 39101) and Group Tag Act (Kind 39102), with detailed specifications for their structure and usage. - Modified the signature generation process to include the canonical WebSocket URL, ensuring proper binding and verification. - Improved validation mechanisms for identity tags and event replication requests, reinforcing security and integrity within the directory consensus protocol. - Added comprehensive documentation for new event types and their respective validation processes, ensuring clarity for developers and users. - Introduced new helper functions and structures to facilitate the creation and management of directory events and acts.
This commit is contained in:
359
pkg/protocol/directory/validation.go
Normal file
359
pkg/protocol/directory/validation.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"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]+)?(?:/.*)?$`)
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user