You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
368 lines
9.6 KiB
368 lines
9.6 KiB
package directory |
|
|
|
import ( |
|
"strconv" |
|
"time" |
|
|
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/errorf" |
|
"next.orly.dev/pkg/encoders/event" |
|
"next.orly.dev/pkg/encoders/tag" |
|
) |
|
|
|
// PublicKeyAdvertisement represents a complete Public Key Advertisement event |
|
// (Kind 39103) with typed access to its components. |
|
type PublicKeyAdvertisement struct { |
|
Event *event.E |
|
KeyID string |
|
PublicKey string |
|
Purpose KeyPurpose |
|
Expiry *time.Time |
|
Algorithm string |
|
DerivationPath string |
|
KeyIndex int |
|
IdentityTag *IdentityTag |
|
} |
|
|
|
// NewPublicKeyAdvertisement creates a new Public Key Advertisement event. |
|
func NewPublicKeyAdvertisement( |
|
pubkey []byte, |
|
keyID, publicKey string, |
|
purpose KeyPurpose, |
|
expiry *time.Time, |
|
algorithm, derivationPath string, |
|
keyIndex int, |
|
identityTag *IdentityTag, |
|
) (pka *PublicKeyAdvertisement, err error) { |
|
|
|
// Validate required fields |
|
if len(pubkey) != 32 { |
|
return nil, errorf.E("pubkey must be 32 bytes") |
|
} |
|
if keyID == "" { |
|
return nil, errorf.E("key ID is required") |
|
} |
|
if publicKey == "" { |
|
return nil, errorf.E("public key is required") |
|
} |
|
if len(publicKey) != 64 { |
|
return nil, errorf.E("public key must be 64 hex characters") |
|
} |
|
if err = ValidateKeyPurpose(string(purpose)); chk.E(err) { |
|
return |
|
} |
|
// Expiry is optional, but if provided, must be in the future |
|
if expiry != nil && expiry.Before(time.Now()) { |
|
return nil, errorf.E("expiry time must be in the future") |
|
} |
|
if algorithm == "" { |
|
algorithm = "secp256k1" // Default algorithm |
|
} |
|
if derivationPath == "" { |
|
return nil, errorf.E("derivation path is required") |
|
} |
|
if keyIndex < 0 { |
|
return nil, errorf.E("key index must be non-negative") |
|
} |
|
|
|
// Validate identity tag if provided |
|
if identityTag != nil { |
|
if err = identityTag.Validate(); chk.E(err) { |
|
return |
|
} |
|
} |
|
|
|
// Create base event |
|
ev := CreateBaseEvent(pubkey, PublicKeyAdvertisementKind) |
|
|
|
// Add required tags |
|
ev.Tags.Append(tag.NewFromAny(string(DTag), keyID)) |
|
ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), publicKey)) |
|
ev.Tags.Append(tag.NewFromAny(string(PurposeTag), string(purpose))) |
|
ev.Tags.Append(tag.NewFromAny(string(AlgorithmTag), algorithm)) |
|
ev.Tags.Append(tag.NewFromAny(string(DerivationPathTag), derivationPath)) |
|
ev.Tags.Append(tag.NewFromAny(string(KeyIndexTag), strconv.Itoa(keyIndex))) |
|
|
|
// Add optional expiry tag |
|
if expiry != nil { |
|
ev.Tags.Append(tag.NewFromAny(string(ExpiryTag), strconv.FormatInt(expiry.Unix(), 10))) |
|
} |
|
|
|
// Add identity tag if provided |
|
if identityTag != nil { |
|
ev.Tags.Append(tag.NewFromAny(string(ITag), |
|
identityTag.NPubIdentity, |
|
identityTag.Nonce, |
|
identityTag.Signature)) |
|
} |
|
|
|
pka = &PublicKeyAdvertisement{ |
|
Event: ev, |
|
KeyID: keyID, |
|
PublicKey: publicKey, |
|
Purpose: purpose, |
|
Expiry: expiry, |
|
Algorithm: algorithm, |
|
DerivationPath: derivationPath, |
|
KeyIndex: keyIndex, |
|
IdentityTag: identityTag, |
|
} |
|
|
|
return |
|
} |
|
|
|
// ParsePublicKeyAdvertisement parses an event into a PublicKeyAdvertisement |
|
// structure with validation. |
|
func ParsePublicKeyAdvertisement(ev *event.E) (pka *PublicKeyAdvertisement, err error) { |
|
if ev == nil { |
|
return nil, errorf.E("event cannot be nil") |
|
} |
|
|
|
// Validate event kind |
|
if ev.Kind != PublicKeyAdvertisementKind.K { |
|
return nil, errorf.E("invalid event kind: expected %d, got %d", |
|
PublicKeyAdvertisementKind.K, ev.Kind) |
|
} |
|
|
|
// Extract required tags |
|
dTag := ev.Tags.GetFirst(DTag) |
|
if dTag == nil { |
|
return nil, errorf.E("missing d tag") |
|
} |
|
|
|
pubkeyTag := ev.Tags.GetFirst(PubkeyTag) |
|
if pubkeyTag == nil { |
|
return nil, errorf.E("missing pubkey tag") |
|
} |
|
|
|
purposeTag := ev.Tags.GetFirst(PurposeTag) |
|
if purposeTag == nil { |
|
return nil, errorf.E("missing purpose tag") |
|
} |
|
|
|
// Parse optional expiry |
|
var expiry *time.Time |
|
expiryTag := ev.Tags.GetFirst(ExpiryTag) |
|
if expiryTag != nil { |
|
var expiryUnix int64 |
|
if expiryUnix, err = strconv.ParseInt(string(expiryTag.Value()), 10, 64); chk.E(err) { |
|
return nil, errorf.E("invalid expiry timestamp: %w", err) |
|
} |
|
expiryTime := time.Unix(expiryUnix, 0) |
|
expiry = &expiryTime |
|
} |
|
|
|
algorithmTag := ev.Tags.GetFirst(AlgorithmTag) |
|
if algorithmTag == nil { |
|
return nil, errorf.E("missing algorithm tag") |
|
} |
|
|
|
derivationPathTag := ev.Tags.GetFirst(DerivationPathTag) |
|
if derivationPathTag == nil { |
|
return nil, errorf.E("missing derivation_path tag") |
|
} |
|
|
|
keyIndexTag := ev.Tags.GetFirst(KeyIndexTag) |
|
if keyIndexTag == nil { |
|
return nil, errorf.E("missing key_index tag") |
|
} |
|
|
|
// Validate and parse purpose |
|
purpose := KeyPurpose(purposeTag.Value()) |
|
if err = ValidateKeyPurpose(string(purpose)); chk.E(err) { |
|
return |
|
} |
|
|
|
// Parse key index |
|
var keyIndex int |
|
if keyIndex, err = strconv.Atoi(string(keyIndexTag.Value())); chk.E(err) { |
|
return nil, errorf.E("invalid key_index: %w", err) |
|
} |
|
|
|
// Parse identity tag (I tag) |
|
var identityTag *IdentityTag |
|
iTag := ev.Tags.GetFirst(ITag) |
|
if iTag != nil { |
|
if identityTag, err = ParseIdentityTag(iTag); chk.E(err) { |
|
return |
|
} |
|
} |
|
|
|
pka = &PublicKeyAdvertisement{ |
|
Event: ev, |
|
KeyID: string(dTag.Value()), |
|
PublicKey: string(pubkeyTag.Value()), |
|
Purpose: purpose, |
|
Expiry: expiry, |
|
Algorithm: string(algorithmTag.Value()), |
|
DerivationPath: string(derivationPathTag.Value()), |
|
KeyIndex: keyIndex, |
|
IdentityTag: identityTag, |
|
} |
|
|
|
return |
|
} |
|
|
|
// Validate performs comprehensive validation of a PublicKeyAdvertisement. |
|
func (pka *PublicKeyAdvertisement) Validate() (err error) { |
|
if pka == nil { |
|
return errorf.E("PublicKeyAdvertisement cannot be nil") |
|
} |
|
|
|
if pka.Event == nil { |
|
return errorf.E("event cannot be nil") |
|
} |
|
|
|
// Validate event signature |
|
if _, err = pka.Event.Verify(); chk.E(err) { |
|
return errorf.E("invalid event signature: %w", err) |
|
} |
|
|
|
// Validate required fields |
|
if pka.KeyID == "" { |
|
return errorf.E("key ID is required") |
|
} |
|
|
|
if pka.PublicKey == "" { |
|
return errorf.E("public key is required") |
|
} |
|
|
|
if len(pka.PublicKey) != 64 { |
|
return errorf.E("public key must be 64 hex characters") |
|
} |
|
|
|
if err = ValidateKeyPurpose(string(pka.Purpose)); chk.E(err) { |
|
return |
|
} |
|
|
|
// Ensure no more mistakes by correcting field usage comprehensively |
|
|
|
// Update relevant parts of the code to use Expiry instead of removed fields. |
|
if pka.Expiry != nil && pka.Expiry.Before(time.Now()) { |
|
return errorf.E("public key advertisement is expired") |
|
} |
|
|
|
// Make sure any logic that checks valid periods is now using the created_at timestamp rather than a specific validity period |
|
// Statements using ValidFrom or ValidUntil should be revised or removed according to the new logic. |
|
|
|
if pka.Algorithm == "" { |
|
return errorf.E("algorithm is required") |
|
} |
|
|
|
if pka.DerivationPath == "" { |
|
return errorf.E("derivation path is required") |
|
} |
|
|
|
if pka.KeyIndex < 0 { |
|
return errorf.E("key index must be non-negative") |
|
} |
|
|
|
// Validate identity tag if present |
|
if pka.IdentityTag != nil { |
|
if err = pka.IdentityTag.Validate(); chk.E(err) { |
|
return |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// IsValid returns true if the key is currently valid (within its validity period). |
|
func (pka *PublicKeyAdvertisement) IsValid() bool { |
|
if pka.Expiry == nil { |
|
return false |
|
} |
|
return time.Now().Before(*pka.Expiry) |
|
} |
|
|
|
// IsExpired returns true if the key has expired. |
|
func (pka *PublicKeyAdvertisement) IsExpired() bool { |
|
if pka.Expiry == nil { |
|
return false |
|
} |
|
return time.Now().After(*pka.Expiry) |
|
} |
|
|
|
// IsNotYetValid returns true if the key is not yet valid. |
|
func (pka *PublicKeyAdvertisement) IsNotYetValid() bool { |
|
if pka.Expiry == nil { |
|
return true // Consider valid if no expiry is set |
|
} |
|
return time.Now().Before(*pka.Expiry) |
|
} |
|
|
|
// TimeUntilExpiry returns the duration until the key expires. |
|
// Returns 0 if already expired. |
|
func (pka *PublicKeyAdvertisement) TimeUntilExpiry() time.Duration { |
|
if pka.Expiry == nil { |
|
return 0 |
|
} |
|
if pka.IsExpired() { |
|
return 0 |
|
} |
|
return time.Until(*pka.Expiry) |
|
} |
|
|
|
// TimeUntilValid returns the duration until the key becomes valid. |
|
// Returns 0 if already valid or expired. |
|
func (pka *PublicKeyAdvertisement) TimeUntilValid() time.Duration { |
|
if !pka.IsNotYetValid() { |
|
return 0 |
|
} |
|
return time.Until(*pka.Expiry) |
|
} |
|
|
|
// GetKeyID returns the unique key identifier. |
|
func (pka *PublicKeyAdvertisement) GetKeyID() string { |
|
return pka.KeyID |
|
} |
|
|
|
// GetPublicKey returns the hex-encoded public key. |
|
func (pka *PublicKeyAdvertisement) GetPublicKey() string { |
|
return pka.PublicKey |
|
} |
|
|
|
// GetPurpose returns the key purpose. |
|
func (pka *PublicKeyAdvertisement) GetPurpose() KeyPurpose { |
|
return pka.Purpose |
|
} |
|
|
|
// GetAlgorithm returns the cryptographic algorithm. |
|
func (pka *PublicKeyAdvertisement) GetAlgorithm() string { |
|
return pka.Algorithm |
|
} |
|
|
|
// GetDerivationPath returns the BIP32 derivation path. |
|
func (pka *PublicKeyAdvertisement) GetDerivationPath() string { |
|
return pka.DerivationPath |
|
} |
|
|
|
// GetKeyIndex returns the key index from the derivation path. |
|
func (pka *PublicKeyAdvertisement) GetKeyIndex() int { |
|
return pka.KeyIndex |
|
} |
|
|
|
// GetIdentityTag returns the identity tag, or nil if not present. |
|
func (pka *PublicKeyAdvertisement) GetIdentityTag() *IdentityTag { |
|
return pka.IdentityTag |
|
} |
|
|
|
// HasPurpose returns true if the key has the specified purpose. |
|
func (pka *PublicKeyAdvertisement) HasPurpose(purpose KeyPurpose) bool { |
|
return pka.Purpose == purpose |
|
} |
|
|
|
// IsSigningKey returns true if this is a signing key. |
|
func (pka *PublicKeyAdvertisement) IsSigningKey() bool { |
|
return pka.Purpose == KeyPurposeSigning |
|
} |
|
|
|
// IsEncryptionKey returns true if this is an encryption key. |
|
func (pka *PublicKeyAdvertisement) IsEncryptionKey() bool { |
|
return pka.Purpose == KeyPurposeEncryption |
|
} |
|
|
|
// IsDelegationKey returns true if this is a delegation key. |
|
func (pka *PublicKeyAdvertisement) IsDelegationKey() bool { |
|
return pka.Purpose == KeyPurposeDelegation |
|
}
|
|
|