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:
368
pkg/protocol/directory/public_key_advertisement.go
Normal file
368
pkg/protocol/directory/public_key_advertisement.go
Normal file
@@ -0,0 +1,368 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user