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