implement messages and operations for FIND
This commit is contained in:
388
pkg/find/builder.go
Normal file
388
pkg/find/builder.go
Normal 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
325
pkg/find/certificate.go
Normal 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
455
pkg/find/parser.go
Normal 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
167
pkg/find/sign.go
Normal 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
168
pkg/find/transfer.go
Normal 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
180
pkg/find/types.go
Normal 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
221
pkg/find/validation.go
Normal 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
317
pkg/find/verify.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user