public_key_advertisement.go raw

   1  package directory
   2  
   3  import (
   4  	"strconv"
   5  	"time"
   6  
   7  	"next.orly.dev/pkg/lol/chk"
   8  	"next.orly.dev/pkg/lol/errorf"
   9  	"next.orly.dev/pkg/nostr/encoders/event"
  10  	"next.orly.dev/pkg/nostr/encoders/tag"
  11  )
  12  
  13  // PublicKeyAdvertisement represents a complete Public Key Advertisement event
  14  // (Kind 39103) with typed access to its components.
  15  type PublicKeyAdvertisement struct {
  16  	Event          *event.E
  17  	KeyID          string
  18  	PublicKey      string
  19  	Purpose        KeyPurpose
  20  	Expiry         *time.Time
  21  	Algorithm      string
  22  	DerivationPath string
  23  	KeyIndex       int
  24  	IdentityTag    *IdentityTag
  25  }
  26  
  27  // NewPublicKeyAdvertisement creates a new Public Key Advertisement event.
  28  func NewPublicKeyAdvertisement(
  29  	pubkey []byte,
  30  	keyID, publicKey string,
  31  	purpose KeyPurpose,
  32  	expiry *time.Time,
  33  	algorithm, derivationPath string,
  34  	keyIndex int,
  35  	identityTag *IdentityTag,
  36  ) (pka *PublicKeyAdvertisement, err error) {
  37  
  38  	// Validate required fields
  39  	if len(pubkey) != 32 {
  40  		return nil, errorf.E("pubkey must be 32 bytes")
  41  	}
  42  	if keyID == "" {
  43  		return nil, errorf.E("key ID is required")
  44  	}
  45  	if publicKey == "" {
  46  		return nil, errorf.E("public key is required")
  47  	}
  48  	if len(publicKey) != 64 {
  49  		return nil, errorf.E("public key must be 64 hex characters")
  50  	}
  51  	if err = ValidateKeyPurpose(string(purpose)); chk.E(err) {
  52  		return
  53  	}
  54  	// Expiry is optional, but if provided, must be in the future
  55  	if expiry != nil && expiry.Before(time.Now()) {
  56  		return nil, errorf.E("expiry time must be in the future")
  57  	}
  58  	if algorithm == "" {
  59  		algorithm = "secp256k1" // Default algorithm
  60  	}
  61  	if derivationPath == "" {
  62  		return nil, errorf.E("derivation path is required")
  63  	}
  64  	if keyIndex < 0 {
  65  		return nil, errorf.E("key index must be non-negative")
  66  	}
  67  
  68  	// Validate identity tag if provided
  69  	if identityTag != nil {
  70  		if err = identityTag.Validate(); chk.E(err) {
  71  			return
  72  		}
  73  	}
  74  
  75  	// Create base event
  76  	ev := CreateBaseEvent(pubkey, PublicKeyAdvertisementKind)
  77  
  78  	// Add required tags
  79  	ev.Tags.Append(tag.NewFromAny(string(DTag), keyID))
  80  	ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), publicKey))
  81  	ev.Tags.Append(tag.NewFromAny(string(PurposeTag), string(purpose)))
  82  	ev.Tags.Append(tag.NewFromAny(string(AlgorithmTag), algorithm))
  83  	ev.Tags.Append(tag.NewFromAny(string(DerivationPathTag), derivationPath))
  84  	ev.Tags.Append(tag.NewFromAny(string(KeyIndexTag), strconv.Itoa(keyIndex)))
  85  
  86  	// Add optional expiry tag
  87  	if expiry != nil {
  88  		ev.Tags.Append(tag.NewFromAny(string(ExpiryTag), strconv.FormatInt(expiry.Unix(), 10)))
  89  	}
  90  
  91  	// Add identity tag if provided
  92  	if identityTag != nil {
  93  		ev.Tags.Append(tag.NewFromAny(string(ITag),
  94  			identityTag.NPubIdentity,
  95  			identityTag.Nonce,
  96  			identityTag.Signature))
  97  	}
  98  
  99  	pka = &PublicKeyAdvertisement{
 100  		Event:          ev,
 101  		KeyID:          keyID,
 102  		PublicKey:      publicKey,
 103  		Purpose:        purpose,
 104  		Expiry:         expiry,
 105  		Algorithm:      algorithm,
 106  		DerivationPath: derivationPath,
 107  		KeyIndex:       keyIndex,
 108  		IdentityTag:    identityTag,
 109  	}
 110  
 111  	return
 112  }
 113  
 114  // ParsePublicKeyAdvertisement parses an event into a PublicKeyAdvertisement
 115  // structure with validation.
 116  func ParsePublicKeyAdvertisement(ev *event.E) (pka *PublicKeyAdvertisement, err error) {
 117  	if ev == nil {
 118  		return nil, errorf.E("event cannot be nil")
 119  	}
 120  
 121  	// Validate event kind
 122  	if ev.Kind != PublicKeyAdvertisementKind.K {
 123  		return nil, errorf.E("invalid event kind: expected %d, got %d",
 124  			PublicKeyAdvertisementKind.K, ev.Kind)
 125  	}
 126  
 127  	// Extract required tags
 128  	dTag := ev.Tags.GetFirst(DTag)
 129  	if dTag == nil {
 130  		return nil, errorf.E("missing d tag")
 131  	}
 132  
 133  	pubkeyTag := ev.Tags.GetFirst(PubkeyTag)
 134  	if pubkeyTag == nil {
 135  		return nil, errorf.E("missing pubkey tag")
 136  	}
 137  
 138  	purposeTag := ev.Tags.GetFirst(PurposeTag)
 139  	if purposeTag == nil {
 140  		return nil, errorf.E("missing purpose tag")
 141  	}
 142  
 143  	// Parse optional expiry
 144  	var expiry *time.Time
 145  	expiryTag := ev.Tags.GetFirst(ExpiryTag)
 146  	if expiryTag != nil {
 147  		var expiryUnix int64
 148  		if expiryUnix, err = strconv.ParseInt(string(expiryTag.Value()), 10, 64); chk.E(err) {
 149  			return nil, errorf.E("invalid expiry timestamp: %w", err)
 150  		}
 151  		expiryTime := time.Unix(expiryUnix, 0)
 152  		expiry = &expiryTime
 153  	}
 154  
 155  	algorithmTag := ev.Tags.GetFirst(AlgorithmTag)
 156  	if algorithmTag == nil {
 157  		return nil, errorf.E("missing algorithm tag")
 158  	}
 159  
 160  	derivationPathTag := ev.Tags.GetFirst(DerivationPathTag)
 161  	if derivationPathTag == nil {
 162  		return nil, errorf.E("missing derivation_path tag")
 163  	}
 164  
 165  	keyIndexTag := ev.Tags.GetFirst(KeyIndexTag)
 166  	if keyIndexTag == nil {
 167  		return nil, errorf.E("missing key_index tag")
 168  	}
 169  
 170  	// Validate and parse purpose
 171  	purpose := KeyPurpose(purposeTag.Value())
 172  	if err = ValidateKeyPurpose(string(purpose)); chk.E(err) {
 173  		return
 174  	}
 175  
 176  	// Parse key index
 177  	var keyIndex int
 178  	if keyIndex, err = strconv.Atoi(string(keyIndexTag.Value())); chk.E(err) {
 179  		return nil, errorf.E("invalid key_index: %w", err)
 180  	}
 181  
 182  	// Parse identity tag (I tag)
 183  	var identityTag *IdentityTag
 184  	iTag := ev.Tags.GetFirst(ITag)
 185  	if iTag != nil {
 186  		if identityTag, err = ParseIdentityTag(iTag); chk.E(err) {
 187  			return
 188  		}
 189  	}
 190  
 191  	pka = &PublicKeyAdvertisement{
 192  		Event:          ev,
 193  		KeyID:          string(dTag.Value()),
 194  		PublicKey:      string(pubkeyTag.Value()),
 195  		Purpose:        purpose,
 196  		Expiry:         expiry,
 197  		Algorithm:      string(algorithmTag.Value()),
 198  		DerivationPath: string(derivationPathTag.Value()),
 199  		KeyIndex:       keyIndex,
 200  		IdentityTag:    identityTag,
 201  	}
 202  
 203  	return
 204  }
 205  
 206  // Validate performs comprehensive validation of a PublicKeyAdvertisement.
 207  func (pka *PublicKeyAdvertisement) Validate() (err error) {
 208  	if pka == nil {
 209  		return errorf.E("PublicKeyAdvertisement cannot be nil")
 210  	}
 211  
 212  	if pka.Event == nil {
 213  		return errorf.E("event cannot be nil")
 214  	}
 215  
 216  	// Validate event signature
 217  	if _, err = pka.Event.Verify(); chk.E(err) {
 218  		return errorf.E("invalid event signature: %w", err)
 219  	}
 220  
 221  	// Validate required fields
 222  	if pka.KeyID == "" {
 223  		return errorf.E("key ID is required")
 224  	}
 225  
 226  	if pka.PublicKey == "" {
 227  		return errorf.E("public key is required")
 228  	}
 229  
 230  	if len(pka.PublicKey) != 64 {
 231  		return errorf.E("public key must be 64 hex characters")
 232  	}
 233  
 234  	if err = ValidateKeyPurpose(string(pka.Purpose)); chk.E(err) {
 235  		return
 236  	}
 237  
 238  	// Ensure no more mistakes by correcting field usage comprehensively
 239  
 240  	// Update relevant parts of the code to use Expiry instead of removed fields.
 241  	if pka.Expiry != nil && pka.Expiry.Before(time.Now()) {
 242  		return errorf.E("public key advertisement is expired")
 243  	}
 244  
 245  	// Make sure any logic that checks valid periods is now using the created_at timestamp rather than a specific validity period
 246  	// Statements using ValidFrom or ValidUntil should be revised or removed according to the new logic.
 247  
 248  	if pka.Algorithm == "" {
 249  		return errorf.E("algorithm is required")
 250  	}
 251  
 252  	if pka.DerivationPath == "" {
 253  		return errorf.E("derivation path is required")
 254  	}
 255  
 256  	if pka.KeyIndex < 0 {
 257  		return errorf.E("key index must be non-negative")
 258  	}
 259  
 260  	// Validate identity tag if present
 261  	if pka.IdentityTag != nil {
 262  		if err = pka.IdentityTag.Validate(); chk.E(err) {
 263  			return
 264  		}
 265  	}
 266  
 267  	return nil
 268  }
 269  
 270  // IsValid returns true if the key is currently valid (within its validity period).
 271  func (pka *PublicKeyAdvertisement) IsValid() bool {
 272  	if pka.Expiry == nil {
 273  		return false
 274  	}
 275  	return time.Now().Before(*pka.Expiry)
 276  }
 277  
 278  // IsExpired returns true if the key has expired.
 279  func (pka *PublicKeyAdvertisement) IsExpired() bool {
 280  	if pka.Expiry == nil {
 281  		return false
 282  	}
 283  	return time.Now().After(*pka.Expiry)
 284  }
 285  
 286  // IsNotYetValid returns true if the key is not yet valid.
 287  func (pka *PublicKeyAdvertisement) IsNotYetValid() bool {
 288  	if pka.Expiry == nil {
 289  		return true // Consider valid if no expiry is set
 290  	}
 291  	return time.Now().Before(*pka.Expiry)
 292  }
 293  
 294  // TimeUntilExpiry returns the duration until the key expires.
 295  // Returns 0 if already expired.
 296  func (pka *PublicKeyAdvertisement) TimeUntilExpiry() time.Duration {
 297  	if pka.Expiry == nil {
 298  		return 0
 299  	}
 300  	if pka.IsExpired() {
 301  		return 0
 302  	}
 303  	return time.Until(*pka.Expiry)
 304  }
 305  
 306  // TimeUntilValid returns the duration until the key becomes valid.
 307  // Returns 0 if already valid or expired.
 308  func (pka *PublicKeyAdvertisement) TimeUntilValid() time.Duration {
 309  	if !pka.IsNotYetValid() {
 310  		return 0
 311  	}
 312  	return time.Until(*pka.Expiry)
 313  }
 314  
 315  // GetKeyID returns the unique key identifier.
 316  func (pka *PublicKeyAdvertisement) GetKeyID() string {
 317  	return pka.KeyID
 318  }
 319  
 320  // GetPublicKey returns the hex-encoded public key.
 321  func (pka *PublicKeyAdvertisement) GetPublicKey() string {
 322  	return pka.PublicKey
 323  }
 324  
 325  // GetPurpose returns the key purpose.
 326  func (pka *PublicKeyAdvertisement) GetPurpose() KeyPurpose {
 327  	return pka.Purpose
 328  }
 329  
 330  // GetAlgorithm returns the cryptographic algorithm.
 331  func (pka *PublicKeyAdvertisement) GetAlgorithm() string {
 332  	return pka.Algorithm
 333  }
 334  
 335  // GetDerivationPath returns the BIP32 derivation path.
 336  func (pka *PublicKeyAdvertisement) GetDerivationPath() string {
 337  	return pka.DerivationPath
 338  }
 339  
 340  // GetKeyIndex returns the key index from the derivation path.
 341  func (pka *PublicKeyAdvertisement) GetKeyIndex() int {
 342  	return pka.KeyIndex
 343  }
 344  
 345  // GetIdentityTag returns the identity tag, or nil if not present.
 346  func (pka *PublicKeyAdvertisement) GetIdentityTag() *IdentityTag {
 347  	return pka.IdentityTag
 348  }
 349  
 350  // HasPurpose returns true if the key has the specified purpose.
 351  func (pka *PublicKeyAdvertisement) HasPurpose(purpose KeyPurpose) bool {
 352  	return pka.Purpose == purpose
 353  }
 354  
 355  // IsSigningKey returns true if this is a signing key.
 356  func (pka *PublicKeyAdvertisement) IsSigningKey() bool {
 357  	return pka.Purpose == KeyPurposeSigning
 358  }
 359  
 360  // IsEncryptionKey returns true if this is an encryption key.
 361  func (pka *PublicKeyAdvertisement) IsEncryptionKey() bool {
 362  	return pka.Purpose == KeyPurposeEncryption
 363  }
 364  
 365  // IsDelegationKey returns true if this is a delegation key.
 366  func (pka *PublicKeyAdvertisement) IsDelegationKey() bool {
 367  	return pka.Purpose == KeyPurposeDelegation
 368  }
 369