implement messages and operations for FIND

This commit is contained in:
2025-11-08 08:54:58 +00:00
parent 587116afa8
commit b4760c49b6
14 changed files with 3225 additions and 17 deletions

388
pkg/find/builder.go Normal file
View File

@@ -0,0 +1,388 @@
package find
import (
"fmt"
"strconv"
"time"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/encoders/timestamp"
"next.orly.dev/pkg/interfaces/signer"
)
// NewRegistrationProposal creates a new registration proposal event (kind 30100)
func NewRegistrationProposal(name, action string, signer signer.I) (*event.E, error) {
// Validate and normalize name
name = NormalizeName(name)
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Validate action
if action != ActionRegister && action != ActionTransfer {
return nil, fmt.Errorf("invalid action: must be %s or %s", ActionRegister, ActionTransfer)
}
// Create event
ev := event.New()
ev.Kind = KindRegistrationProposal
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("d", name))
tags.Append(tag.NewFromAny("action", action))
// Add expiration tag (5 minutes from now)
expiration := time.Now().Add(ProposalExpiry).Unix()
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
ev.Tags = tags
ev.Content = []byte{}
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign event: %w", err)
}
return ev, nil
}
// NewRegistrationProposalWithTransfer creates a transfer proposal with previous owner signature
func NewRegistrationProposalWithTransfer(name, prevOwner, prevSig string, signer signer.I) (*event.E, error) {
// Create base proposal
ev, err := NewRegistrationProposal(name, ActionTransfer, signer)
if err != nil {
return nil, err
}
// Add transfer-specific tags
ev.Tags.Append(tag.NewFromAny("prev_owner", prevOwner))
ev.Tags.Append(tag.NewFromAny("prev_sig", prevSig))
// Re-sign after adding tags
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign transfer event: %w", err)
}
return ev, nil
}
// NewAttestation creates a new attestation event (kind 20100)
func NewAttestation(proposalID, decision string, weight int, reason, serviceURL string, signer signer.I) (*event.E, error) {
// Validate decision
if decision != DecisionApprove && decision != DecisionReject && decision != DecisionAbstain {
return nil, fmt.Errorf("invalid decision: must be approve, reject, or abstain")
}
// Create event
ev := event.New()
ev.Kind = KindAttestation
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("e", proposalID))
tags.Append(tag.NewFromAny("decision", decision))
if weight > 0 {
tags.Append(tag.NewFromAny("weight", strconv.Itoa(weight)))
}
if reason != "" {
tags.Append(tag.NewFromAny("reason", reason))
}
if serviceURL != "" {
tags.Append(tag.NewFromAny("service", serviceURL))
}
// Add expiration tag (3 minutes from now)
expiration := time.Now().Add(AttestationExpiry).Unix()
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
ev.Tags = tags
ev.Content = []byte{}
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign attestation: %w", err)
}
return ev, nil
}
// NewTrustGraph creates a new trust graph event (kind 30101)
func NewTrustGraph(entries []TrustEntry, signer signer.I) (*event.E, error) {
// Validate trust entries
for i, entry := range entries {
if err := ValidateTrustScore(entry.TrustScore); err != nil {
return nil, fmt.Errorf("invalid trust score at index %d: %w", i, err)
}
}
// Create event
ev := event.New()
ev.Kind = KindTrustGraph
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("d", "trust-graph"))
// Add trust entries as p tags
for _, entry := range entries {
tags.Append(tag.NewFromAny("p", entry.Pubkey, entry.ServiceURL,
strconv.FormatFloat(entry.TrustScore, 'f', 2, 64)))
}
// Add expiration tag (30 days from now)
expiration := time.Now().Add(TrustGraphExpiry).Unix()
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
ev.Tags = tags
ev.Content = []byte{}
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign trust graph: %w", err)
}
return ev, nil
}
// NewNameState creates a new name state event (kind 30102)
func NewNameState(name, owner string, registeredAt time.Time, proposalID string,
attestations int, confidence float64, signer signer.I) (*event.E, error) {
// Validate name
name = NormalizeName(name)
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Create event
ev := event.New()
ev.Kind = KindNameState
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("d", name))
tags.Append(tag.NewFromAny("owner", owner))
tags.Append(tag.NewFromAny("registered_at", strconv.FormatInt(registeredAt.Unix(), 10)))
tags.Append(tag.NewFromAny("proposal", proposalID))
tags.Append(tag.NewFromAny("attestations", strconv.Itoa(attestations)))
tags.Append(tag.NewFromAny("confidence", strconv.FormatFloat(confidence, 'f', 2, 64)))
// Add expiration tag (1 year from registration)
expiration := registeredAt.Add(NameRegistrationPeriod).Unix()
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
ev.Tags = tags
ev.Content = []byte{}
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign name state: %w", err)
}
return ev, nil
}
// NewNameRecord creates a new name record event (kind 30103)
func NewNameRecord(name, recordType, value string, ttl int, signer signer.I) (*event.E, error) {
// Validate name
name = NormalizeName(name)
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Validate record value
if err := ValidateRecordValue(recordType, value); err != nil {
return nil, err
}
// Create event
ev := event.New()
ev.Kind = KindNameRecords
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("d", fmt.Sprintf("%s:%s", name, recordType)))
tags.Append(tag.NewFromAny("name", name))
tags.Append(tag.NewFromAny("type", recordType))
tags.Append(tag.NewFromAny("value", value))
if ttl > 0 {
tags.Append(tag.NewFromAny("ttl", strconv.Itoa(ttl)))
}
ev.Tags = tags
ev.Content = []byte{}
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign name record: %w", err)
}
return ev, nil
}
// NewNameRecordWithPriority creates a name record with priority (for MX, SRV)
func NewNameRecordWithPriority(name, recordType, value string, ttl, priority int, signer signer.I) (*event.E, error) {
// Validate priority
if err := ValidatePriority(priority); err != nil {
return nil, err
}
// Create base record
ev, err := NewNameRecord(name, recordType, value, ttl, signer)
if err != nil {
return nil, err
}
// Add priority tag
ev.Tags.Append(tag.NewFromAny("priority", strconv.Itoa(priority)))
// Re-sign
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign record with priority: %w", err)
}
return ev, nil
}
// NewSRVRecord creates an SRV record with all required fields
func NewSRVRecord(name, value string, ttl, priority, weight, port int, signer signer.I) (*event.E, error) {
// Validate SRV-specific fields
if err := ValidatePriority(priority); err != nil {
return nil, err
}
if err := ValidateWeight(weight); err != nil {
return nil, err
}
if err := ValidatePort(port); err != nil {
return nil, err
}
// Create base record
ev, err := NewNameRecord(name, RecordTypeSRV, value, ttl, signer)
if err != nil {
return nil, err
}
// Add SRV-specific tags
ev.Tags.Append(tag.NewFromAny("priority", strconv.Itoa(priority)))
ev.Tags.Append(tag.NewFromAny("weight", strconv.Itoa(weight)))
ev.Tags.Append(tag.NewFromAny("port", strconv.Itoa(port)))
// Re-sign
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign SRV record: %w", err)
}
return ev, nil
}
// NewCertificate creates a new certificate event (kind 30104)
func NewCertificate(name, certPubkey string, validFrom, validUntil time.Time,
challenge, challengeProof string, witnesses []WitnessSignature,
algorithm, usage string, signer signer.I) (*event.E, error) {
// Validate name
name = NormalizeName(name)
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Create event
ev := event.New()
ev.Kind = KindCertificate
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("d", name))
tags.Append(tag.NewFromAny("name", name))
tags.Append(tag.NewFromAny("cert_pubkey", certPubkey))
tags.Append(tag.NewFromAny("valid_from", strconv.FormatInt(validFrom.Unix(), 10)))
tags.Append(tag.NewFromAny("valid_until", strconv.FormatInt(validUntil.Unix(), 10)))
tags.Append(tag.NewFromAny("challenge", challenge))
tags.Append(tag.NewFromAny("challenge_proof", challengeProof))
// Add witness signatures
for _, w := range witnesses {
tags.Append(tag.NewFromAny("witness", w.Pubkey, w.Signature))
}
ev.Tags = tags
// Add metadata to content
content := fmt.Sprintf(`{"algorithm":"%s","usage":"%s"}`, algorithm, usage)
ev.Content = []byte(content)
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign certificate: %w", err)
}
return ev, nil
}
// NewWitnessService creates a new witness service info event (kind 30105)
func NewWitnessService(endpoint string, challenges []string, maxValidity, fee int,
reputationID, description, contact string, signer signer.I) (*event.E, error) {
// Create event
ev := event.New()
ev.Kind = KindWitnessService
ev.CreatedAt = timestamp.Now().V
ev.Pubkey = signer.Pub()
// Build tags
tags := tag.NewS()
tags.Append(tag.NewFromAny("d", "witness-service"))
tags.Append(tag.NewFromAny("endpoint", endpoint))
for _, ch := range challenges {
tags.Append(tag.NewFromAny("challenges", ch))
}
if maxValidity > 0 {
tags.Append(tag.NewFromAny("max_validity", strconv.Itoa(maxValidity)))
}
if fee > 0 {
tags.Append(tag.NewFromAny("fee", strconv.Itoa(fee)))
}
if reputationID != "" {
tags.Append(tag.NewFromAny("reputation", reputationID))
}
// Add expiration tag (180 days from now)
expiration := time.Now().Add(WitnessServiceExpiry).Unix()
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
ev.Tags = tags
// Add metadata to content
content := fmt.Sprintf(`{"description":"%s","contact":"%s"}`, description, contact)
ev.Content = []byte(content)
// Sign the event
if err := ev.Sign(signer); err != nil {
return nil, fmt.Errorf("failed to sign witness service: %w", err)
}
return ev, nil
}

325
pkg/find/certificate.go Normal file
View File

@@ -0,0 +1,325 @@
package find
import (
"crypto/rand"
"fmt"
"time"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/interfaces/signer"
)
// GenerateChallenge generates a random 32-byte challenge token
func GenerateChallenge() (string, error) {
challenge := make([]byte, 32)
if _, err := rand.Read(challenge); err != nil {
return "", fmt.Errorf("failed to generate random challenge: %w", err)
}
return hex.Enc(challenge), nil
}
// CreateChallengeTXTRecord creates a TXT record event for challenge-response verification
func CreateChallengeTXTRecord(name, challenge string, ttl int, signer signer.I) (*event.E, error) {
// Normalize name
name = NormalizeName(name)
// Validate name
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Create TXT record value
txtValue := fmt.Sprintf("_nostr-challenge=%s", challenge)
// Create the TXT record event
record, err := NewNameRecord(name, RecordTypeTXT, txtValue, ttl, signer)
if err != nil {
return nil, fmt.Errorf("failed to create challenge TXT record: %w", err)
}
return record, nil
}
// ExtractChallengeFromTXTRecord extracts the challenge token from a TXT record value
func ExtractChallengeFromTXTRecord(txtValue string) (string, error) {
const prefix = "_nostr-challenge="
if len(txtValue) < len(prefix) {
return "", fmt.Errorf("TXT record too short")
}
if txtValue[:len(prefix)] != prefix {
return "", fmt.Errorf("not a challenge TXT record")
}
challenge := txtValue[len(prefix):]
if len(challenge) != 64 { // 32 bytes in hex = 64 characters
return "", fmt.Errorf("invalid challenge length: %d", len(challenge))
}
return challenge, nil
}
// CreateChallengeProof creates a challenge proof signature
func CreateChallengeProof(challenge, name, certPubkey string, validUntil time.Time, signer signer.I) (string, error) {
// Normalize name
name = NormalizeName(name)
// Sign the challenge proof
proof, err := SignChallengeProof(challenge, name, certPubkey, validUntil, signer)
if err != nil {
return "", fmt.Errorf("failed to create challenge proof: %w", err)
}
return proof, nil
}
// RequestWitnessSignature creates a witness signature for a certificate
// This would typically be called by a witness service
func RequestWitnessSignature(cert *Certificate, witnessSigner signer.I) (WitnessSignature, error) {
// Sign the witness message
sig, err := SignWitnessMessage(cert.CertPubkey, cert.Name,
cert.ValidFrom, cert.ValidUntil, cert.Challenge, witnessSigner)
if err != nil {
return WitnessSignature{}, fmt.Errorf("failed to create witness signature: %w", err)
}
// Get witness pubkey
witnessPubkey := hex.Enc(witnessSigner.Pub())
return WitnessSignature{
Pubkey: witnessPubkey,
Signature: sig,
}, nil
}
// PrepareCertificateRequest prepares all the data needed for a certificate request
type CertificateRequest struct {
Name string
CertPubkey string
ValidFrom time.Time
ValidUntil time.Time
Challenge string
ChallengeProof string
}
// CreateCertificateRequest creates a certificate request with challenge-response
func CreateCertificateRequest(name, certPubkey string, validityDuration time.Duration,
challenge string, ownerSigner signer.I) (*CertificateRequest, error) {
// Normalize name
name = NormalizeName(name)
// Validate name
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Set validity period
validFrom := time.Now()
validUntil := validFrom.Add(validityDuration)
// Create challenge proof
proof, err := CreateChallengeProof(challenge, name, certPubkey, validUntil, ownerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create challenge proof: %w", err)
}
return &CertificateRequest{
Name: name,
CertPubkey: certPubkey,
ValidFrom: validFrom,
ValidUntil: validUntil,
Challenge: challenge,
ChallengeProof: proof,
}, nil
}
// CreateCertificateWithWitnesses creates a complete certificate event with witness signatures
func CreateCertificateWithWitnesses(req *CertificateRequest, witnesses []WitnessSignature,
algorithm, usage string, ownerSigner signer.I) (*event.E, error) {
// Create the certificate event
certEvent, err := NewCertificate(
req.Name,
req.CertPubkey,
req.ValidFrom,
req.ValidUntil,
req.Challenge,
req.ChallengeProof,
witnesses,
algorithm,
usage,
ownerSigner,
)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
return certEvent, nil
}
// VerifyChallengeTXTRecord verifies that a TXT record contains the expected challenge
func VerifyChallengeTXTRecord(record *NameRecord, expectedChallenge string, nameOwner string) error {
// Check record type
if record.Type != RecordTypeTXT {
return fmt.Errorf("not a TXT record: %s", record.Type)
}
// Check record owner matches name owner
recordOwner := hex.Enc(record.Event.Pubkey)
if recordOwner != nameOwner {
return fmt.Errorf("record owner %s != name owner %s", recordOwner, nameOwner)
}
// Extract challenge from TXT record
challenge, err := ExtractChallengeFromTXTRecord(record.Value)
if err != nil {
return fmt.Errorf("failed to extract challenge: %w", err)
}
// Verify challenge matches
if challenge != expectedChallenge {
return fmt.Errorf("challenge mismatch: got %s, expected %s", challenge, expectedChallenge)
}
return nil
}
// IssueCertificate is a helper that goes through the full certificate issuance process
// This would typically be used by a name owner to request a certificate
func IssueCertificate(name, certPubkey string, validityDuration time.Duration,
ownerSigner signer.I, witnessSigners []signer.I) (*Certificate, error) {
// Generate challenge
challenge, err := GenerateChallenge()
if err != nil {
return nil, fmt.Errorf("failed to generate challenge: %w", err)
}
// Create certificate request
req, err := CreateCertificateRequest(name, certPubkey, validityDuration, challenge, ownerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create certificate request: %w", err)
}
// Collect witness signatures
var witnesses []WitnessSignature
for i, ws := range witnessSigners {
// Create temporary certificate for witness to sign
tempCert := &Certificate{
Name: req.Name,
CertPubkey: req.CertPubkey,
ValidFrom: req.ValidFrom,
ValidUntil: req.ValidUntil,
Challenge: req.Challenge,
}
witness, err := RequestWitnessSignature(tempCert, ws)
if err != nil {
return nil, fmt.Errorf("failed to get witness %d signature: %w", i, err)
}
witnesses = append(witnesses, witness)
}
// Create certificate event
certEvent, err := CreateCertificateWithWitnesses(req, witnesses, "secp256k1-schnorr", "tls-replacement", ownerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create certificate event: %w", err)
}
// Parse back to Certificate struct
cert, err := ParseCertificate(certEvent)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
// RenewCertificate creates a renewed certificate with a new validity period
func RenewCertificate(oldCert *Certificate, newValidityDuration time.Duration,
ownerSigner signer.I, witnessSigners []signer.I) (*Certificate, error) {
// Generate new challenge
challenge, err := GenerateChallenge()
if err != nil {
return nil, fmt.Errorf("failed to generate challenge: %w", err)
}
// Set new validity period (with 7-day overlap)
validFrom := oldCert.ValidUntil.Add(-7 * 24 * time.Hour)
validUntil := validFrom.Add(newValidityDuration)
// Create challenge proof
proof, err := CreateChallengeProof(challenge, oldCert.Name, oldCert.CertPubkey, validUntil, ownerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create challenge proof: %w", err)
}
// Create request
req := &CertificateRequest{
Name: oldCert.Name,
CertPubkey: oldCert.CertPubkey,
ValidFrom: validFrom,
ValidUntil: validUntil,
Challenge: challenge,
ChallengeProof: proof,
}
// Collect witness signatures
var witnesses []WitnessSignature
for i, ws := range witnessSigners {
tempCert := &Certificate{
Name: req.Name,
CertPubkey: req.CertPubkey,
ValidFrom: req.ValidFrom,
ValidUntil: req.ValidUntil,
Challenge: req.Challenge,
}
witness, err := RequestWitnessSignature(tempCert, ws)
if err != nil {
return nil, fmt.Errorf("failed to get witness %d signature: %w", i, err)
}
witnesses = append(witnesses, witness)
}
// Create certificate event
certEvent, err := CreateCertificateWithWitnesses(req, witnesses, oldCert.Algorithm, oldCert.Usage, ownerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create certificate event: %w", err)
}
// Parse back to Certificate struct
cert, err := ParseCertificate(certEvent)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
// CheckCertificateExpiry returns the time until expiration, or error if expired
func CheckCertificateExpiry(cert *Certificate) (time.Duration, error) {
now := time.Now()
if now.After(cert.ValidUntil) {
return 0, fmt.Errorf("certificate expired %v ago", now.Sub(cert.ValidUntil))
}
return cert.ValidUntil.Sub(now), nil
}
// ShouldRenewCertificate checks if a certificate should be renewed (< 30 days until expiry)
func ShouldRenewCertificate(cert *Certificate) bool {
timeUntilExpiry, err := CheckCertificateExpiry(cert)
if err != nil {
return true // Expired, definitely should renew
}
return timeUntilExpiry < 30*24*time.Hour
}

455
pkg/find/parser.go Normal file
View File

@@ -0,0 +1,455 @@
package find
import (
"encoding/json"
"fmt"
"strconv"
"time"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// getTagValue retrieves the value of the first tag with the given key
func getTagValue(ev *event.E, key string) string {
t := ev.Tags.GetFirst([]byte(key))
if t == nil {
return ""
}
return string(t.Value())
}
// getAllTags retrieves all tags with the given key
func getAllTags(ev *event.E, key string) []*tag.T {
return ev.Tags.GetAll([]byte(key))
}
// ParseRegistrationProposal parses a kind 30100 event into a RegistrationProposal
func ParseRegistrationProposal(ev *event.E) (*RegistrationProposal, error) {
if uint16(ev.Kind) != KindRegistrationProposal {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindRegistrationProposal, ev.Kind)
}
name := getTagValue(ev, "d")
if name == "" {
return nil, fmt.Errorf("missing 'd' tag (name)")
}
action := getTagValue(ev, "action")
if action == "" {
return nil, fmt.Errorf("missing 'action' tag")
}
expirationStr := getTagValue(ev, "expiration")
var expiration time.Time
if expirationStr != "" {
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
}
expiration = time.Unix(expirationUnix, 0)
}
proposal := &RegistrationProposal{
Event: ev,
Name: name,
Action: action,
PrevOwner: getTagValue(ev, "prev_owner"),
PrevSig: getTagValue(ev, "prev_sig"),
Expiration: expiration,
}
return proposal, nil
}
// ParseAttestation parses a kind 20100 event into an Attestation
func ParseAttestation(ev *event.E) (*Attestation, error) {
if uint16(ev.Kind) != KindAttestation {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindAttestation, ev.Kind)
}
proposalID := getTagValue(ev, "e")
if proposalID == "" {
return nil, fmt.Errorf("missing 'e' tag (proposal ID)")
}
decision := getTagValue(ev, "decision")
if decision == "" {
return nil, fmt.Errorf("missing 'decision' tag")
}
weightStr := getTagValue(ev, "weight")
weight := 100 // default weight
if weightStr != "" {
w, err := strconv.Atoi(weightStr)
if err != nil {
return nil, fmt.Errorf("invalid weight value: %w", err)
}
weight = w
}
expirationStr := getTagValue(ev, "expiration")
var expiration time.Time
if expirationStr != "" {
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
}
expiration = time.Unix(expirationUnix, 0)
}
attestation := &Attestation{
Event: ev,
ProposalID: proposalID,
Decision: decision,
Weight: weight,
Reason: getTagValue(ev, "reason"),
ServiceURL: getTagValue(ev, "service"),
Expiration: expiration,
}
return attestation, nil
}
// ParseTrustGraph parses a kind 30101 event into a TrustGraph
func ParseTrustGraph(ev *event.E) (*TrustGraph, error) {
if uint16(ev.Kind) != KindTrustGraph {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindTrustGraph, ev.Kind)
}
expirationStr := getTagValue(ev, "expiration")
var expiration time.Time
if expirationStr != "" {
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
}
expiration = time.Unix(expirationUnix, 0)
}
// Parse p tags (trust entries)
var entries []TrustEntry
pTags := getAllTags(ev, "p")
for _, t := range pTags {
if len(t.T) < 2 {
continue // Skip malformed tags
}
pubkey := string(t.T[1])
serviceURL := ""
trustScore := 0.5 // default
if len(t.T) > 2 {
serviceURL = string(t.T[2])
}
if len(t.T) > 3 {
score, err := strconv.ParseFloat(string(t.T[3]), 64)
if err == nil {
trustScore = score
}
}
entries = append(entries, TrustEntry{
Pubkey: pubkey,
ServiceURL: serviceURL,
TrustScore: trustScore,
})
}
return &TrustGraph{
Event: ev,
Entries: entries,
Expiration: expiration,
}, nil
}
// ParseNameState parses a kind 30102 event into a NameState
func ParseNameState(ev *event.E) (*NameState, error) {
if uint16(ev.Kind) != KindNameState {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindNameState, ev.Kind)
}
name := getTagValue(ev, "d")
if name == "" {
return nil, fmt.Errorf("missing 'd' tag (name)")
}
owner := getTagValue(ev, "owner")
if owner == "" {
return nil, fmt.Errorf("missing 'owner' tag")
}
registeredAtStr := getTagValue(ev, "registered_at")
if registeredAtStr == "" {
return nil, fmt.Errorf("missing 'registered_at' tag")
}
registeredAtUnix, err := strconv.ParseInt(registeredAtStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid registered_at timestamp: %w", err)
}
registeredAt := time.Unix(registeredAtUnix, 0)
attestationsStr := getTagValue(ev, "attestations")
attestations := 0
if attestationsStr != "" {
a, err := strconv.Atoi(attestationsStr)
if err == nil {
attestations = a
}
}
confidenceStr := getTagValue(ev, "confidence")
confidence := 0.0
if confidenceStr != "" {
c, err := strconv.ParseFloat(confidenceStr, 64)
if err == nil {
confidence = c
}
}
expirationStr := getTagValue(ev, "expiration")
var expiration time.Time
if expirationStr != "" {
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
}
expiration = time.Unix(expirationUnix, 0)
}
return &NameState{
Event: ev,
Name: name,
Owner: owner,
RegisteredAt: registeredAt,
ProposalID: getTagValue(ev, "proposal"),
Attestations: attestations,
Confidence: confidence,
Expiration: expiration,
}, nil
}
// ParseNameRecord parses a kind 30103 event into a NameRecord
func ParseNameRecord(ev *event.E) (*NameRecord, error) {
if uint16(ev.Kind) != KindNameRecords {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindNameRecords, ev.Kind)
}
name := getTagValue(ev, "name")
if name == "" {
return nil, fmt.Errorf("missing 'name' tag")
}
recordType := getTagValue(ev, "type")
if recordType == "" {
return nil, fmt.Errorf("missing 'type' tag")
}
value := getTagValue(ev, "value")
if value == "" {
return nil, fmt.Errorf("missing 'value' tag")
}
ttlStr := getTagValue(ev, "ttl")
ttl := 3600 // default TTL
if ttlStr != "" {
t, err := strconv.Atoi(ttlStr)
if err == nil {
ttl = t
}
}
priorityStr := getTagValue(ev, "priority")
priority := 0
if priorityStr != "" {
p, err := strconv.Atoi(priorityStr)
if err == nil {
priority = p
}
}
weightStr := getTagValue(ev, "weight")
weight := 0
if weightStr != "" {
w, err := strconv.Atoi(weightStr)
if err == nil {
weight = w
}
}
portStr := getTagValue(ev, "port")
port := 0
if portStr != "" {
p, err := strconv.Atoi(portStr)
if err == nil {
port = p
}
}
return &NameRecord{
Event: ev,
Name: name,
Type: recordType,
Value: value,
TTL: ttl,
Priority: priority,
Weight: weight,
Port: port,
}, nil
}
// ParseCertificate parses a kind 30104 event into a Certificate
func ParseCertificate(ev *event.E) (*Certificate, error) {
if uint16(ev.Kind) != KindCertificate {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindCertificate, ev.Kind)
}
name := getTagValue(ev, "name")
if name == "" {
return nil, fmt.Errorf("missing 'name' tag")
}
certPubkey := getTagValue(ev, "cert_pubkey")
if certPubkey == "" {
return nil, fmt.Errorf("missing 'cert_pubkey' tag")
}
validFromStr := getTagValue(ev, "valid_from")
if validFromStr == "" {
return nil, fmt.Errorf("missing 'valid_from' tag")
}
validFromUnix, err := strconv.ParseInt(validFromStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid valid_from timestamp: %w", err)
}
validFrom := time.Unix(validFromUnix, 0)
validUntilStr := getTagValue(ev, "valid_until")
if validUntilStr == "" {
return nil, fmt.Errorf("missing 'valid_until' tag")
}
validUntilUnix, err := strconv.ParseInt(validUntilStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid valid_until timestamp: %w", err)
}
validUntil := time.Unix(validUntilUnix, 0)
// Parse witness tags
var witnesses []WitnessSignature
witnessTags := getAllTags(ev, "witness")
for _, t := range witnessTags {
if len(t.T) < 3 {
continue // Skip malformed tags
}
witnesses = append(witnesses, WitnessSignature{
Pubkey: string(t.T[1]),
Signature: string(t.T[2]),
})
}
// Parse content JSON
algorithm := "secp256k1-schnorr"
usage := "tls-replacement"
if len(ev.Content) > 0 {
var metadata map[string]interface{}
if err := json.Unmarshal(ev.Content, &metadata); err == nil {
if alg, ok := metadata["algorithm"].(string); ok {
algorithm = alg
}
if u, ok := metadata["usage"].(string); ok {
usage = u
}
}
}
return &Certificate{
Event: ev,
Name: name,
CertPubkey: certPubkey,
ValidFrom: validFrom,
ValidUntil: validUntil,
Challenge: getTagValue(ev, "challenge"),
ChallengeProof: getTagValue(ev, "challenge_proof"),
Witnesses: witnesses,
Algorithm: algorithm,
Usage: usage,
}, nil
}
// ParseWitnessService parses a kind 30105 event into a WitnessService
func ParseWitnessService(ev *event.E) (*WitnessService, error) {
if uint16(ev.Kind) != KindWitnessService {
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindWitnessService, ev.Kind)
}
endpoint := getTagValue(ev, "endpoint")
if endpoint == "" {
return nil, fmt.Errorf("missing 'endpoint' tag")
}
// Parse challenge tags
var challenges []string
challengeTags := getAllTags(ev, "challenges")
for _, t := range challengeTags {
if len(t.T) >= 2 {
challenges = append(challenges, string(t.T[1]))
}
}
maxValidityStr := getTagValue(ev, "max_validity")
maxValidity := 0
if maxValidityStr != "" {
mv, err := strconv.Atoi(maxValidityStr)
if err == nil {
maxValidity = mv
}
}
feeStr := getTagValue(ev, "fee")
fee := 0
if feeStr != "" {
f, err := strconv.Atoi(feeStr)
if err == nil {
fee = f
}
}
expirationStr := getTagValue(ev, "expiration")
var expiration time.Time
if expirationStr != "" {
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
}
expiration = time.Unix(expirationUnix, 0)
}
// Parse content JSON
description := ""
contact := ""
if len(ev.Content) > 0 {
var metadata map[string]interface{}
if err := json.Unmarshal(ev.Content, &metadata); err == nil {
if desc, ok := metadata["description"].(string); ok {
description = desc
}
if cont, ok := metadata["contact"].(string); ok {
contact = cont
}
}
}
return &WitnessService{
Event: ev,
Endpoint: endpoint,
Challenges: challenges,
MaxValidity: maxValidity,
Fee: fee,
ReputationID: getTagValue(ev, "reputation"),
Description: description,
Contact: contact,
Expiration: expiration,
}, nil
}

167
pkg/find/sign.go Normal file
View File

@@ -0,0 +1,167 @@
package find
import (
"crypto/sha256"
"fmt"
"time"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/interfaces/signer"
)
// SignTransferAuth creates a signature for transfer authorization
// Message format: transfer:<name>:<new_owner_pubkey>:<timestamp>
func SignTransferAuth(name, newOwner string, timestamp time.Time, s signer.I) (string, error) {
// Normalize name
name = NormalizeName(name)
// Construct message
message := fmt.Sprintf("transfer:%s:%s:%d", name, newOwner, timestamp.Unix())
// Hash the message
hash := sha256.Sum256([]byte(message))
// Sign the hash
sig, err := s.Sign(hash[:])
if err != nil {
return "", fmt.Errorf("failed to sign transfer authorization: %w", err)
}
// Return hex-encoded signature
return hex.Enc(sig), nil
}
// SignChallengeProof creates a signature for certificate challenge proof
// Message format: challenge||name||cert_pubkey||valid_until
func SignChallengeProof(challenge, name, certPubkey string, validUntil time.Time, s signer.I) (string, error) {
// Normalize name
name = NormalizeName(name)
// Construct message
message := fmt.Sprintf("%s||%s||%s||%d", challenge, name, certPubkey, validUntil.Unix())
// Hash the message
hash := sha256.Sum256([]byte(message))
// Sign the hash
sig, err := s.Sign(hash[:])
if err != nil {
return "", fmt.Errorf("failed to sign challenge proof: %w", err)
}
// Return hex-encoded signature
return hex.Enc(sig), nil
}
// SignWitnessMessage creates a witness signature for a certificate
// Message format: cert_pubkey||name||valid_from||valid_until||challenge
func SignWitnessMessage(certPubkey, name string, validFrom, validUntil time.Time, challenge string, s signer.I) (string, error) {
// Normalize name
name = NormalizeName(name)
// Construct message
message := fmt.Sprintf("%s||%s||%d||%d||%s",
certPubkey, name, validFrom.Unix(), validUntil.Unix(), challenge)
// Hash the message
hash := sha256.Sum256([]byte(message))
// Sign the hash
sig, err := s.Sign(hash[:])
if err != nil {
return "", fmt.Errorf("failed to sign witness message: %w", err)
}
// Return hex-encoded signature
return hex.Enc(sig), nil
}
// CreateTransferAuthMessage constructs the transfer authorization message
// This is used for verification
func CreateTransferAuthMessage(name, newOwner string, timestamp time.Time) []byte {
name = NormalizeName(name)
message := fmt.Sprintf("transfer:%s:%s:%d", name, newOwner, timestamp.Unix())
hash := sha256.Sum256([]byte(message))
return hash[:]
}
// CreateChallengeProofMessage constructs the challenge proof message
// This is used for verification
func CreateChallengeProofMessage(challenge, name, certPubkey string, validUntil time.Time) []byte {
name = NormalizeName(name)
message := fmt.Sprintf("%s||%s||%s||%d", challenge, name, certPubkey, validUntil.Unix())
hash := sha256.Sum256([]byte(message))
return hash[:]
}
// CreateWitnessMessage constructs the witness message
// This is used for verification
func CreateWitnessMessage(certPubkey, name string, validFrom, validUntil time.Time, challenge string) []byte {
name = NormalizeName(name)
message := fmt.Sprintf("%s||%s||%d||%d||%s",
certPubkey, name, validFrom.Unix(), validUntil.Unix(), challenge)
hash := sha256.Sum256([]byte(message))
return hash[:]
}
// ParseTimestampFromProposal extracts the timestamp from a transfer authorization message
// Used for verification when the timestamp is embedded in the signature
func ParseTimestampFromProposal(proposalTime time.Time) time.Time {
// Round to nearest second for consistency
return proposalTime.Truncate(time.Second)
}
// FormatTransferAuthString formats the transfer auth message for display/debugging
func FormatTransferAuthString(name, newOwner string, timestamp time.Time) string {
name = NormalizeName(name)
return fmt.Sprintf("transfer:%s:%s:%d", name, newOwner, timestamp.Unix())
}
// FormatChallengeProofString formats the challenge proof message for display/debugging
func FormatChallengeProofString(challenge, name, certPubkey string, validUntil time.Time) string {
name = NormalizeName(name)
return fmt.Sprintf("%s||%s||%s||%d", challenge, name, certPubkey, validUntil.Unix())
}
// FormatWitnessString formats the witness message for display/debugging
func FormatWitnessString(certPubkey, name string, validFrom, validUntil time.Time, challenge string) string {
name = NormalizeName(name)
return fmt.Sprintf("%s||%s||%d||%d||%s",
certPubkey, name, validFrom.Unix(), validUntil.Unix(), challenge)
}
// SignProposal signs a registration proposal event
func SignProposal(ev *event.E, s signer.I) error {
return ev.Sign(s)
}
// SignAttestation signs an attestation event
func SignAttestation(ev *event.E, s signer.I) error {
return ev.Sign(s)
}
// SignTrustGraph signs a trust graph event
func SignTrustGraph(ev *event.E, s signer.I) error {
return ev.Sign(s)
}
// SignNameState signs a name state event
func SignNameState(ev *event.E, s signer.I) error {
return ev.Sign(s)
}
// SignNameRecord signs a name record event
func SignNameRecord(ev *event.E, s signer.I) error {
return ev.Sign(s)
}
// SignCertificate signs a certificate event
func SignCertificate(ev *event.E, s signer.I) error {
return ev.Sign(s)
}
// SignWitnessService signs a witness service event
func SignWitnessService(ev *event.E, s signer.I) error {
return ev.Sign(s)
}

168
pkg/find/transfer.go Normal file
View File

@@ -0,0 +1,168 @@
package find
import (
"fmt"
"time"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/interfaces/signer"
)
// CreateTransferProposal creates a complete transfer proposal with authorization from previous owner
func CreateTransferProposal(name string, prevOwnerSigner, newOwnerSigner signer.I) (*event.E, error) {
// Normalize name
name = NormalizeName(name)
// Validate name
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Get public keys
prevOwnerPubkey := hex.Enc(prevOwnerSigner.Pub())
newOwnerPubkey := hex.Enc(newOwnerSigner.Pub())
// Create timestamp for the transfer
timestamp := time.Now()
// Sign the transfer authorization with previous owner's key
prevSig, err := SignTransferAuth(name, newOwnerPubkey, timestamp, prevOwnerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create transfer authorization: %w", err)
}
// Create the transfer proposal event signed by new owner
proposal, err := NewRegistrationProposalWithTransfer(name, prevOwnerPubkey, prevSig, newOwnerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create transfer proposal: %w", err)
}
return proposal, nil
}
// ValidateTransferProposal validates a transfer proposal against the current owner
func ValidateTransferProposal(proposal *RegistrationProposal, currentOwner string) error {
// Check that this is a transfer action
if proposal.Action != ActionTransfer {
return fmt.Errorf("not a transfer action: %s", proposal.Action)
}
// Check that prev_owner is set
if proposal.PrevOwner == "" {
return fmt.Errorf("missing prev_owner in transfer proposal")
}
// Check that prev_sig is set
if proposal.PrevSig == "" {
return fmt.Errorf("missing prev_sig in transfer proposal")
}
// Verify that prev_owner matches current owner
if proposal.PrevOwner != currentOwner {
return fmt.Errorf("prev_owner %s does not match current owner %s",
proposal.PrevOwner, currentOwner)
}
// Get new owner from proposal event
newOwnerPubkey := hex.Enc(proposal.Event.Pubkey)
// Verify the transfer authorization signature
// Use proposal creation time as timestamp
timestamp := time.Unix(proposal.Event.CreatedAt, 0)
ok, err := VerifyTransferAuth(proposal.Name, newOwnerPubkey, proposal.PrevOwner,
timestamp, proposal.PrevSig)
if err != nil {
return fmt.Errorf("transfer authorization verification failed: %w", err)
}
if !ok {
return fmt.Errorf("invalid transfer authorization signature")
}
return nil
}
// PrepareTransferAuth prepares the transfer authorization data that needs to be signed
// This is a helper for wallets/clients that want to show what they're signing
func PrepareTransferAuth(name, newOwner string, timestamp time.Time) TransferAuthorization {
return TransferAuthorization{
Name: NormalizeName(name),
NewOwner: newOwner,
Timestamp: timestamp,
}
}
// AuthorizeTransfer creates a transfer authorization signature
// This is meant to be used by the current owner to authorize a transfer to a new owner
func AuthorizeTransfer(name, newOwnerPubkey string, ownerSigner signer.I) (prevSig string, timestamp time.Time, err error) {
// Normalize name
name = NormalizeName(name)
// Validate name
if err := ValidateName(name); err != nil {
return "", time.Time{}, fmt.Errorf("invalid name: %w", err)
}
// Create timestamp
timestamp = time.Now()
// Sign the authorization
prevSig, err = SignTransferAuth(name, newOwnerPubkey, timestamp, ownerSigner)
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to sign transfer auth: %w", err)
}
return prevSig, timestamp, nil
}
// CreateTransferProposalWithAuth creates a transfer proposal using a pre-existing authorization
// This is useful when the previous owner has already provided their signature
func CreateTransferProposalWithAuth(name, prevOwnerPubkey, prevSig string, newOwnerSigner signer.I) (*event.E, error) {
// Normalize name
name = NormalizeName(name)
// Validate name
if err := ValidateName(name); err != nil {
return nil, fmt.Errorf("invalid name: %w", err)
}
// Create the transfer proposal event
proposal, err := NewRegistrationProposalWithTransfer(name, prevOwnerPubkey, prevSig, newOwnerSigner)
if err != nil {
return nil, fmt.Errorf("failed to create transfer proposal: %w", err)
}
return proposal, nil
}
// VerifyTransferProposalSignature verifies both the event signature and transfer authorization
func VerifyTransferProposalSignature(proposal *RegistrationProposal) error {
// Verify the event signature itself
if err := VerifyEvent(proposal.Event); err != nil {
return fmt.Errorf("invalid event signature: %w", err)
}
// If this is a transfer, verify the transfer authorization
if proposal.Action == ActionTransfer {
// Get new owner from proposal event
newOwnerPubkey := hex.Enc(proposal.Event.Pubkey)
// Use proposal creation time as timestamp
timestamp := time.Unix(proposal.Event.CreatedAt, 0)
// Verify transfer auth
ok, err := VerifyTransferAuth(proposal.Name, newOwnerPubkey, proposal.PrevOwner,
timestamp, proposal.PrevSig)
if err != nil {
return fmt.Errorf("transfer authorization verification failed: %w", err)
}
if !ok {
return fmt.Errorf("invalid transfer authorization signature")
}
}
return nil
}

180
pkg/find/types.go Normal file
View File

@@ -0,0 +1,180 @@
package find
import (
"time"
"next.orly.dev/pkg/encoders/event"
)
// Event kind constants as defined in the NIP
const (
KindRegistrationProposal = 30100 // Parameterized replaceable
KindAttestation = 20100 // Ephemeral
KindTrustGraph = 30101 // Parameterized replaceable
KindNameState = 30102 // Parameterized replaceable
KindNameRecords = 30103 // Parameterized replaceable
KindCertificate = 30104 // Parameterized replaceable
KindWitnessService = 30105 // Parameterized replaceable
)
// Action types for registration proposals
const (
ActionRegister = "register"
ActionTransfer = "transfer"
)
// Decision types for attestations
const (
DecisionApprove = "approve"
DecisionReject = "reject"
DecisionAbstain = "abstain"
)
// DNS record types
const (
RecordTypeA = "A"
RecordTypeAAAA = "AAAA"
RecordTypeCNAME = "CNAME"
RecordTypeMX = "MX"
RecordTypeTXT = "TXT"
RecordTypeNS = "NS"
RecordTypeSRV = "SRV"
)
// Time constants
const (
ProposalExpiry = 5 * time.Minute // Proposals expire after 5 minutes
AttestationExpiry = 3 * time.Minute // Attestations expire after 3 minutes
TrustGraphExpiry = 30 * 24 * time.Hour // Trust graphs expire after 30 days
NameRegistrationPeriod = 365 * 24 * time.Hour // Names expire after 1 year
PreferentialRenewalDays = 30 // Final 30 days before expiration
CertificateValidity = 90 * 24 * time.Hour // Recommended certificate validity
WitnessServiceExpiry = 180 * 24 * time.Hour // Witness service info expires after 180 days
)
// RegistrationProposal represents a kind 30100 event
type RegistrationProposal struct {
Event *event.E
Name string
Action string // "register" or "transfer"
PrevOwner string // Previous owner pubkey (for transfers)
PrevSig string // Signature from previous owner (for transfers)
Expiration time.Time
}
// Attestation represents a kind 20100 event
type Attestation struct {
Event *event.E
ProposalID string // Event ID of the proposal being attested
Decision string // "approve", "reject", or "abstain"
Weight int // Stake/confidence weight (default 100)
Reason string // Human-readable justification
ServiceURL string // Registry service endpoint
Expiration time.Time
}
// TrustEntry represents a single trust relationship
type TrustEntry struct {
Pubkey string
ServiceURL string
TrustScore float64 // 0.0 to 1.0
}
// TrustGraph represents a kind 30101 event
type TrustGraph struct {
Event *event.E
Entries []TrustEntry
Expiration time.Time
}
// NameState represents a kind 30102 event
type NameState struct {
Event *event.E
Name string
Owner string // Current owner pubkey
RegisteredAt time.Time
ProposalID string // Event ID of the registration proposal
Attestations int // Number of attestations
Confidence float64 // Consensus confidence score (0.0 to 1.0)
Expiration time.Time
}
// NameRecord represents a kind 30103 event
type NameRecord struct {
Event *event.E
Name string
Type string // A, AAAA, CNAME, MX, TXT, NS, SRV
Value string
TTL int // Cache TTL in seconds
Priority int // For MX and SRV records
Weight int // For SRV records
Port int // For SRV records
}
// RecordLimits defines per-type record limits
var RecordLimits = map[string]int{
RecordTypeA: 5,
RecordTypeAAAA: 5,
RecordTypeCNAME: 1,
RecordTypeMX: 5,
RecordTypeTXT: 10,
RecordTypeNS: 5,
RecordTypeSRV: 10,
}
// Certificate represents a kind 30104 event
type Certificate struct {
Event *event.E
Name string
CertPubkey string // Public key for the service
ValidFrom time.Time
ValidUntil time.Time
Challenge string // Challenge token for ownership proof
ChallengeProof string // Signature over challenge
Witnesses []WitnessSignature
Algorithm string // e.g., "secp256k1-schnorr"
Usage string // e.g., "tls-replacement"
}
// WitnessSignature represents a witness attestation on a certificate
type WitnessSignature struct {
Pubkey string
Signature string
}
// WitnessService represents a kind 30105 event
type WitnessService struct {
Event *event.E
Endpoint string
Challenges []string // Supported challenge types: "txt", "http", "event"
MaxValidity int // Maximum certificate validity in seconds
Fee int // Fee in sats per certificate
ReputationID string // Event ID of reputation event
Description string
Contact string
Expiration time.Time
}
// TransferAuthorization represents the message signed for transfer authorization
type TransferAuthorization struct {
Name string
NewOwner string
Timestamp time.Time
}
// ChallengeProofMessage represents the message signed for certificate challenge proof
type ChallengeProofMessage struct {
Challenge string
Name string
CertPubkey string
ValidUntil time.Time
}
// WitnessMessage represents the message signed by witnesses
type WitnessMessage struct {
CertPubkey string
Name string
ValidFrom time.Time
ValidUntil time.Time
Challenge string
}

221
pkg/find/validation.go Normal file
View File

@@ -0,0 +1,221 @@
package find
import (
"errors"
"fmt"
"regexp"
"strings"
)
var (
ErrInvalidName = errors.New("invalid name format")
ErrNameTooLong = errors.New("name exceeds 253 characters")
ErrLabelTooLong = errors.New("label exceeds 63 characters")
ErrLabelEmpty = errors.New("label is empty")
ErrInvalidCharacter = errors.New("invalid character in name")
ErrInvalidHyphen = errors.New("label cannot start or end with hyphen")
ErrAllNumericLabel = errors.New("label cannot be all numeric")
ErrInvalidRecordValue = errors.New("invalid record value")
ErrRecordLimitExceeded = errors.New("record limit exceeded")
ErrNotOwner = errors.New("not the name owner")
ErrNameExpired = errors.New("name registration expired")
ErrInRenewalWindow = errors.New("name is in renewal window")
ErrNotRenewalWindow = errors.New("not in renewal window")
)
// Name format validation regex
var (
labelRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`)
allNumeric = regexp.MustCompile(`^[0-9]+$`)
)
// NormalizeName converts a name to lowercase
func NormalizeName(name string) string {
return strings.ToLower(name)
}
// ValidateName validates a name according to DNS naming rules
func ValidateName(name string) error {
// Normalize to lowercase
name = NormalizeName(name)
// Check total length
if len(name) > 253 {
return fmt.Errorf("%w: %d > 253", ErrNameTooLong, len(name))
}
if len(name) == 0 {
return fmt.Errorf("%w: name is empty", ErrInvalidName)
}
// Split into labels
labels := strings.Split(name, ".")
for i, label := range labels {
if err := validateLabel(label); err != nil {
return fmt.Errorf("invalid label %d (%s): %w", i, label, err)
}
}
return nil
}
// validateLabel validates a single label according to DNS rules
func validateLabel(label string) error {
// Check length
if len(label) == 0 {
return ErrLabelEmpty
}
if len(label) > 63 {
return fmt.Errorf("%w: %d > 63", ErrLabelTooLong, len(label))
}
// Check character set and hyphen placement
if !labelRegex.MatchString(label) {
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
return ErrInvalidHyphen
}
return ErrInvalidCharacter
}
// Check not all numeric
if allNumeric.MatchString(label) {
return ErrAllNumericLabel
}
return nil
}
// GetParentDomain returns the parent domain of a name
// e.g., "www.example.com" -> "example.com", "example.com" -> "com", "com" -> ""
func GetParentDomain(name string) string {
name = NormalizeName(name)
parts := strings.Split(name, ".")
if len(parts) <= 1 {
return "" // TLD has no parent
}
return strings.Join(parts[1:], ".")
}
// IsTLD returns true if the name is a top-level domain (single label)
func IsTLD(name string) bool {
name = NormalizeName(name)
return !strings.Contains(name, ".")
}
// ValidateIPv4 validates an IPv4 address format
func ValidateIPv4(ip string) error {
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return fmt.Errorf("%w: invalid IPv4 format", ErrInvalidRecordValue)
}
for _, part := range parts {
var octet int
if _, err := fmt.Sscanf(part, "%d", &octet); err != nil {
return fmt.Errorf("%w: invalid IPv4 octet: %v", ErrInvalidRecordValue, err)
}
if octet < 0 || octet > 255 {
return fmt.Errorf("%w: IPv4 octet out of range: %d", ErrInvalidRecordValue, octet)
}
}
return nil
}
// ValidateIPv6 validates an IPv6 address format (simplified check)
func ValidateIPv6(ip string) error {
// Basic validation - contains colons and valid hex characters
if !strings.Contains(ip, ":") {
return fmt.Errorf("%w: invalid IPv6 format", ErrInvalidRecordValue)
}
// Split by colons
parts := strings.Split(ip, ":")
if len(parts) < 3 || len(parts) > 8 {
return fmt.Errorf("%w: invalid IPv6 segment count", ErrInvalidRecordValue)
}
// Check for valid hex characters
validHex := regexp.MustCompile(`^[0-9a-fA-F]*$`)
for _, part := range parts {
if part == "" {
continue // Allow :: notation
}
if len(part) > 4 {
return fmt.Errorf("%w: IPv6 segment too long", ErrInvalidRecordValue)
}
if !validHex.MatchString(part) {
return fmt.Errorf("%w: invalid IPv6 hex", ErrInvalidRecordValue)
}
}
return nil
}
// ValidateRecordValue validates a record value based on its type
func ValidateRecordValue(recordType, value string) error {
switch recordType {
case RecordTypeA:
return ValidateIPv4(value)
case RecordTypeAAAA:
return ValidateIPv6(value)
case RecordTypeCNAME, RecordTypeMX, RecordTypeNS:
return ValidateName(value)
case RecordTypeTXT:
if len(value) > 1024 {
return fmt.Errorf("%w: TXT record exceeds 1024 characters", ErrInvalidRecordValue)
}
return nil
case RecordTypeSRV:
return ValidateName(value) // Hostname for SRV
default:
return fmt.Errorf("%w: unknown record type: %s", ErrInvalidRecordValue, recordType)
}
}
// ValidateRecordLimit checks if adding a record would exceed type limits
func ValidateRecordLimit(recordType string, currentCount int) error {
limit, ok := RecordLimits[recordType]
if !ok {
return fmt.Errorf("%w: unknown record type: %s", ErrInvalidRecordValue, recordType)
}
if currentCount >= limit {
return fmt.Errorf("%w: %s records limited to %d", ErrRecordLimitExceeded, recordType, limit)
}
return nil
}
// ValidatePriority validates priority value (0-65535)
func ValidatePriority(priority int) error {
if priority < 0 || priority > 65535 {
return fmt.Errorf("%w: priority must be 0-65535", ErrInvalidRecordValue)
}
return nil
}
// ValidateWeight validates weight value (0-65535)
func ValidateWeight(weight int) error {
if weight < 0 || weight > 65535 {
return fmt.Errorf("%w: weight must be 0-65535", ErrInvalidRecordValue)
}
return nil
}
// ValidatePort validates port value (0-65535)
func ValidatePort(port int) error {
if port < 0 || port > 65535 {
return fmt.Errorf("%w: port must be 0-65535", ErrInvalidRecordValue)
}
return nil
}
// ValidateTrustScore validates trust score (0.0-1.0)
func ValidateTrustScore(score float64) error {
if score < 0.0 || score > 1.0 {
return fmt.Errorf("trust score must be between 0.0 and 1.0, got %f", score)
}
return nil
}

317
pkg/find/verify.go Normal file
View File

@@ -0,0 +1,317 @@
package find
import (
"fmt"
"time"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/interfaces/signer/p8k"
)
// VerifyEvent verifies the signature of a Nostr event
func VerifyEvent(ev *event.E) error {
ok, err := ev.Verify()
if err != nil {
return fmt.Errorf("signature verification failed: %w", err)
}
if !ok {
return fmt.Errorf("invalid signature")
}
return nil
}
// VerifyTransferAuth verifies a transfer authorization signature
func VerifyTransferAuth(name, newOwner, prevOwner string, timestamp time.Time, sigHex string) (bool, error) {
// Create the message
msgHash := CreateTransferAuthMessage(name, newOwner, timestamp)
// Decode signature
sig, err := hex.Dec(sigHex)
if err != nil {
return false, fmt.Errorf("invalid signature hex: %w", err)
}
// Decode pubkey
pubkey, err := hex.Dec(prevOwner)
if err != nil {
return false, fmt.Errorf("invalid pubkey hex: %w", err)
}
// Create verifier with public key
verifier, err := p8k.New()
if err != nil {
return false, fmt.Errorf("failed to create verifier: %w", err)
}
if err := verifier.InitPub(pubkey); err != nil {
return false, fmt.Errorf("failed to init pubkey: %w", err)
}
// Verify signature
ok, err := verifier.Verify(msgHash, sig)
if err != nil {
return false, fmt.Errorf("verification failed: %w", err)
}
return ok, nil
}
// VerifyChallengeProof verifies a certificate challenge proof signature
func VerifyChallengeProof(challenge, name, certPubkey, owner string, validUntil time.Time, sigHex string) (bool, error) {
// Create the message
msgHash := CreateChallengeProofMessage(challenge, name, certPubkey, validUntil)
// Decode signature
sig, err := hex.Dec(sigHex)
if err != nil {
return false, fmt.Errorf("invalid signature hex: %w", err)
}
// Decode pubkey
pubkey, err := hex.Dec(owner)
if err != nil {
return false, fmt.Errorf("invalid pubkey hex: %w", err)
}
// Create verifier with public key
verifier, err := p8k.New()
if err != nil {
return false, fmt.Errorf("failed to create verifier: %w", err)
}
if err := verifier.InitPub(pubkey); err != nil {
return false, fmt.Errorf("failed to init pubkey: %w", err)
}
// Verify signature
ok, err := verifier.Verify(msgHash, sig)
if err != nil {
return false, fmt.Errorf("verification failed: %w", err)
}
return ok, nil
}
// VerifyWitnessSignature verifies a witness signature on a certificate
func VerifyWitnessSignature(certPubkey, name string, validFrom, validUntil time.Time,
challenge, witnessPubkey, sigHex string) (bool, error) {
// Create the message
msgHash := CreateWitnessMessage(certPubkey, name, validFrom, validUntil, challenge)
// Decode signature
sig, err := hex.Dec(sigHex)
if err != nil {
return false, fmt.Errorf("invalid signature hex: %w", err)
}
// Decode pubkey
pubkey, err := hex.Dec(witnessPubkey)
if err != nil {
return false, fmt.Errorf("invalid pubkey hex: %w", err)
}
// Create verifier with public key
verifier, err := p8k.New()
if err != nil {
return false, fmt.Errorf("failed to create verifier: %w", err)
}
if err := verifier.InitPub(pubkey); err != nil {
return false, fmt.Errorf("failed to init pubkey: %w", err)
}
// Verify signature
ok, err := verifier.Verify(msgHash, sig)
if err != nil {
return false, fmt.Errorf("verification failed: %w", err)
}
return ok, nil
}
// VerifyNameOwnership checks if a record's owner matches the name state owner
func VerifyNameOwnership(nameState *NameState, record *NameRecord) error {
recordOwner := hex.Enc(record.Event.Pubkey)
if recordOwner != nameState.Owner {
return fmt.Errorf("%w: record owner %s != name owner %s",
ErrNotOwner, recordOwner, nameState.Owner)
}
return nil
}
// IsExpired checks if a time-based expiration has passed
func IsExpired(expiration time.Time) bool {
return time.Now().After(expiration)
}
// IsInRenewalWindow checks if the current time is within the preferential renewal window
// (final 30 days before expiration)
func IsInRenewalWindow(expiration time.Time) bool {
now := time.Now()
renewalWindowStart := expiration.Add(-PreferentialRenewalDays * 24 * time.Hour)
return now.After(renewalWindowStart) && now.Before(expiration)
}
// CanRegister checks if a name can be registered based on its state and expiration
func CanRegister(nameState *NameState, proposerPubkey string) error {
// If no name state exists, anyone can register
if nameState == nil {
return nil
}
// Check if name is expired
if IsExpired(nameState.Expiration) {
// Name is expired, anyone can register
return nil
}
// Check if in renewal window
if IsInRenewalWindow(nameState.Expiration) {
// Only current owner can register during renewal window
if proposerPubkey != nameState.Owner {
return ErrInRenewalWindow
}
return nil
}
// Name is still owned and not in renewal window
return fmt.Errorf("name is owned by %s until %s", nameState.Owner, nameState.Expiration)
}
// VerifyProposalExpiration checks if a proposal has expired
func VerifyProposalExpiration(proposal *RegistrationProposal) error {
if !proposal.Expiration.IsZero() && IsExpired(proposal.Expiration) {
return fmt.Errorf("proposal expired at %s", proposal.Expiration)
}
return nil
}
// VerifyAttestationExpiration checks if an attestation has expired
func VerifyAttestationExpiration(attestation *Attestation) error {
if !attestation.Expiration.IsZero() && IsExpired(attestation.Expiration) {
return fmt.Errorf("attestation expired at %s", attestation.Expiration)
}
return nil
}
// VerifyTrustGraphExpiration checks if a trust graph has expired
func VerifyTrustGraphExpiration(trustGraph *TrustGraph) error {
if !trustGraph.Expiration.IsZero() && IsExpired(trustGraph.Expiration) {
return fmt.Errorf("trust graph expired at %s", trustGraph.Expiration)
}
return nil
}
// VerifyNameStateExpiration checks if a name state has expired
func VerifyNameStateExpiration(nameState *NameState) error {
if !nameState.Expiration.IsZero() && IsExpired(nameState.Expiration) {
return ErrNameExpired
}
return nil
}
// VerifyCertificateValidity checks if a certificate is currently valid
func VerifyCertificateValidity(cert *Certificate) error {
now := time.Now()
if now.Before(cert.ValidFrom) {
return fmt.Errorf("certificate not yet valid (valid from %s)", cert.ValidFrom)
}
if now.After(cert.ValidUntil) {
return fmt.Errorf("certificate expired at %s", cert.ValidUntil)
}
return nil
}
// VerifyCertificate performs complete certificate verification
func VerifyCertificate(cert *Certificate, nameState *NameState, trustedWitnesses []string) error {
// Verify certificate is not expired
if err := VerifyCertificateValidity(cert); err != nil {
return err
}
// Verify name is not expired
if err := VerifyNameStateExpiration(nameState); err != nil {
return err
}
// Verify certificate owner matches name owner
certOwner := hex.Enc(cert.Event.Pubkey)
if certOwner != nameState.Owner {
return fmt.Errorf("certificate owner %s != name owner %s", certOwner, nameState.Owner)
}
// Verify challenge proof
ok, err := VerifyChallengeProof(cert.Challenge, cert.Name, cert.CertPubkey,
nameState.Owner, cert.ValidUntil, cert.ChallengeProof)
if err != nil {
return fmt.Errorf("challenge proof verification failed: %w", err)
}
if !ok {
return fmt.Errorf("invalid challenge proof signature")
}
// Count trusted witnesses
trustedCount := 0
for _, witness := range cert.Witnesses {
// Check if witness is in trusted list
isTrusted := false
for _, trusted := range trustedWitnesses {
if witness.Pubkey == trusted {
isTrusted = true
break
}
}
if !isTrusted {
continue
}
// Verify witness signature
ok, err := VerifyWitnessSignature(cert.CertPubkey, cert.Name,
cert.ValidFrom, cert.ValidUntil, cert.Challenge,
witness.Pubkey, witness.Signature)
if err != nil {
return fmt.Errorf("witness %s signature verification failed: %w", witness.Pubkey, err)
}
if !ok {
return fmt.Errorf("invalid witness %s signature", witness.Pubkey)
}
trustedCount++
}
// Require at least 3 trusted witnesses
if trustedCount < 3 {
return fmt.Errorf("insufficient trusted witnesses: %d < 3", trustedCount)
}
return nil
}
// VerifySubdomainAuthority checks if the proposer owns the parent domain
func VerifySubdomainAuthority(name string, proposerPubkey string, parentNameState *NameState) error {
parent := GetParentDomain(name)
// TLDs have no parent
if parent == "" {
return nil
}
// Parent must exist
if parentNameState == nil {
return fmt.Errorf("parent domain %s does not exist", parent)
}
// Proposer must own parent
if proposerPubkey != parentNameState.Owner {
return fmt.Errorf("proposer %s does not own parent domain %s (owner: %s)",
proposerPubkey, parent, parentNameState.Owner)
}
return nil
}