certificate.go raw

   1  package find
   2  
   3  import (
   4  	"crypto/rand"
   5  	"fmt"
   6  	"time"
   7  
   8  	"next.orly.dev/pkg/nostr/encoders/event"
   9  	"next.orly.dev/pkg/nostr/encoders/hex"
  10  	"next.orly.dev/pkg/nostr/interfaces/signer"
  11  )
  12  
  13  // GenerateChallenge generates a random 32-byte challenge token
  14  func GenerateChallenge() (string, error) {
  15  	challenge := make([]byte, 32)
  16  	if _, err := rand.Read(challenge); err != nil {
  17  		return "", fmt.Errorf("failed to generate random challenge: %w", err)
  18  	}
  19  	return hex.Enc(challenge), nil
  20  }
  21  
  22  // CreateChallengeTXTRecord creates a TXT record event for challenge-response verification
  23  func CreateChallengeTXTRecord(name, challenge string, ttl int, signer signer.I) (*event.E, error) {
  24  	// Normalize name
  25  	name = NormalizeName(name)
  26  
  27  	// Validate name
  28  	if err := ValidateName(name); err != nil {
  29  		return nil, fmt.Errorf("invalid name: %w", err)
  30  	}
  31  
  32  	// Create TXT record value
  33  	txtValue := fmt.Sprintf("_nostr-challenge=%s", challenge)
  34  
  35  	// Create the TXT record event
  36  	record, err := NewNameRecord(name, RecordTypeTXT, txtValue, ttl, signer)
  37  	if err != nil {
  38  		return nil, fmt.Errorf("failed to create challenge TXT record: %w", err)
  39  	}
  40  
  41  	return record, nil
  42  }
  43  
  44  // ExtractChallengeFromTXTRecord extracts the challenge token from a TXT record value
  45  func ExtractChallengeFromTXTRecord(txtValue string) (string, error) {
  46  	const prefix = "_nostr-challenge="
  47  
  48  	if len(txtValue) < len(prefix) {
  49  		return "", fmt.Errorf("TXT record too short")
  50  	}
  51  
  52  	if txtValue[:len(prefix)] != prefix {
  53  		return "", fmt.Errorf("not a challenge TXT record")
  54  	}
  55  
  56  	challenge := txtValue[len(prefix):]
  57  	if len(challenge) != 64 { // 32 bytes in hex = 64 characters
  58  		return "", fmt.Errorf("invalid challenge length: %d", len(challenge))
  59  	}
  60  
  61  	return challenge, nil
  62  }
  63  
  64  // CreateChallengeProof creates a challenge proof signature
  65  func CreateChallengeProof(challenge, name, certPubkey string, validUntil time.Time, signer signer.I) (string, error) {
  66  	// Normalize name
  67  	name = NormalizeName(name)
  68  
  69  	// Sign the challenge proof
  70  	proof, err := SignChallengeProof(challenge, name, certPubkey, validUntil, signer)
  71  	if err != nil {
  72  		return "", fmt.Errorf("failed to create challenge proof: %w", err)
  73  	}
  74  
  75  	return proof, nil
  76  }
  77  
  78  // RequestWitnessSignature creates a witness signature for a certificate
  79  // This would typically be called by a witness service
  80  func RequestWitnessSignature(cert *Certificate, witnessSigner signer.I) (WitnessSignature, error) {
  81  	// Sign the witness message
  82  	sig, err := SignWitnessMessage(cert.CertPubkey, cert.Name,
  83  		cert.ValidFrom, cert.ValidUntil, cert.Challenge, witnessSigner)
  84  	if err != nil {
  85  		return WitnessSignature{}, fmt.Errorf("failed to create witness signature: %w", err)
  86  	}
  87  
  88  	// Get witness pubkey
  89  	witnessPubkey := hex.Enc(witnessSigner.Pub())
  90  
  91  	return WitnessSignature{
  92  		Pubkey:    witnessPubkey,
  93  		Signature: sig,
  94  	}, nil
  95  }
  96  
  97  // PrepareCertificateRequest prepares all the data needed for a certificate request
  98  type CertificateRequest struct {
  99  	Name           string
 100  	CertPubkey     string
 101  	ValidFrom      time.Time
 102  	ValidUntil     time.Time
 103  	Challenge      string
 104  	ChallengeProof string
 105  }
 106  
 107  // CreateCertificateRequest creates a certificate request with challenge-response
 108  func CreateCertificateRequest(name, certPubkey string, validityDuration time.Duration,
 109  	challenge string, ownerSigner signer.I) (*CertificateRequest, error) {
 110  
 111  	// Normalize name
 112  	name = NormalizeName(name)
 113  
 114  	// Validate name
 115  	if err := ValidateName(name); err != nil {
 116  		return nil, fmt.Errorf("invalid name: %w", err)
 117  	}
 118  
 119  	// Set validity period
 120  	validFrom := time.Now()
 121  	validUntil := validFrom.Add(validityDuration)
 122  
 123  	// Create challenge proof
 124  	proof, err := CreateChallengeProof(challenge, name, certPubkey, validUntil, ownerSigner)
 125  	if err != nil {
 126  		return nil, fmt.Errorf("failed to create challenge proof: %w", err)
 127  	}
 128  
 129  	return &CertificateRequest{
 130  		Name:           name,
 131  		CertPubkey:     certPubkey,
 132  		ValidFrom:      validFrom,
 133  		ValidUntil:     validUntil,
 134  		Challenge:      challenge,
 135  		ChallengeProof: proof,
 136  	}, nil
 137  }
 138  
 139  // CreateCertificateWithWitnesses creates a complete certificate event with witness signatures
 140  func CreateCertificateWithWitnesses(req *CertificateRequest, witnesses []WitnessSignature,
 141  	algorithm, usage string, ownerSigner signer.I) (*event.E, error) {
 142  
 143  	// Create the certificate event
 144  	certEvent, err := NewCertificate(
 145  		req.Name,
 146  		req.CertPubkey,
 147  		req.ValidFrom,
 148  		req.ValidUntil,
 149  		req.Challenge,
 150  		req.ChallengeProof,
 151  		witnesses,
 152  		algorithm,
 153  		usage,
 154  		ownerSigner,
 155  	)
 156  	if err != nil {
 157  		return nil, fmt.Errorf("failed to create certificate: %w", err)
 158  	}
 159  
 160  	return certEvent, nil
 161  }
 162  
 163  // VerifyChallengeTXTRecord verifies that a TXT record contains the expected challenge
 164  func VerifyChallengeTXTRecord(record *NameRecord, expectedChallenge string, nameOwner string) error {
 165  	// Check record type
 166  	if record.Type != RecordTypeTXT {
 167  		return fmt.Errorf("not a TXT record: %s", record.Type)
 168  	}
 169  
 170  	// Check record owner matches name owner
 171  	recordOwner := hex.Enc(record.Event.Pubkey)
 172  	if recordOwner != nameOwner {
 173  		return fmt.Errorf("record owner %s != name owner %s", recordOwner, nameOwner)
 174  	}
 175  
 176  	// Extract challenge from TXT record
 177  	challenge, err := ExtractChallengeFromTXTRecord(record.Value)
 178  	if err != nil {
 179  		return fmt.Errorf("failed to extract challenge: %w", err)
 180  	}
 181  
 182  	// Verify challenge matches
 183  	if challenge != expectedChallenge {
 184  		return fmt.Errorf("challenge mismatch: got %s, expected %s", challenge, expectedChallenge)
 185  	}
 186  
 187  	return nil
 188  }
 189  
 190  // IssueCertificate is a helper that goes through the full certificate issuance process
 191  // This would typically be used by a name owner to request a certificate
 192  func IssueCertificate(name, certPubkey string, validityDuration time.Duration,
 193  	ownerSigner signer.I, witnessSigners []signer.I) (*Certificate, error) {
 194  
 195  	// Generate challenge
 196  	challenge, err := GenerateChallenge()
 197  	if err != nil {
 198  		return nil, fmt.Errorf("failed to generate challenge: %w", err)
 199  	}
 200  
 201  	// Create certificate request
 202  	req, err := CreateCertificateRequest(name, certPubkey, validityDuration, challenge, ownerSigner)
 203  	if err != nil {
 204  		return nil, fmt.Errorf("failed to create certificate request: %w", err)
 205  	}
 206  
 207  	// Collect witness signatures
 208  	var witnesses []WitnessSignature
 209  	for i, ws := range witnessSigners {
 210  		// Create temporary certificate for witness to sign
 211  		tempCert := &Certificate{
 212  			Name:       req.Name,
 213  			CertPubkey: req.CertPubkey,
 214  			ValidFrom:  req.ValidFrom,
 215  			ValidUntil: req.ValidUntil,
 216  			Challenge:  req.Challenge,
 217  		}
 218  
 219  		witness, err := RequestWitnessSignature(tempCert, ws)
 220  		if err != nil {
 221  			return nil, fmt.Errorf("failed to get witness %d signature: %w", i, err)
 222  		}
 223  
 224  		witnesses = append(witnesses, witness)
 225  	}
 226  
 227  	// Create certificate event
 228  	certEvent, err := CreateCertificateWithWitnesses(req, witnesses, "secp256k1-schnorr", "tls-replacement", ownerSigner)
 229  	if err != nil {
 230  		return nil, fmt.Errorf("failed to create certificate event: %w", err)
 231  	}
 232  
 233  	// Parse back to Certificate struct
 234  	cert, err := ParseCertificate(certEvent)
 235  	if err != nil {
 236  		return nil, fmt.Errorf("failed to parse certificate: %w", err)
 237  	}
 238  
 239  	return cert, nil
 240  }
 241  
 242  // RenewCertificate creates a renewed certificate with a new validity period
 243  func RenewCertificate(oldCert *Certificate, newValidityDuration time.Duration,
 244  	ownerSigner signer.I, witnessSigners []signer.I) (*Certificate, error) {
 245  
 246  	// Generate new challenge
 247  	challenge, err := GenerateChallenge()
 248  	if err != nil {
 249  		return nil, fmt.Errorf("failed to generate challenge: %w", err)
 250  	}
 251  
 252  	// Set new validity period (with 7-day overlap)
 253  	validFrom := oldCert.ValidUntil.Add(-7 * 24 * time.Hour)
 254  	validUntil := validFrom.Add(newValidityDuration)
 255  
 256  	// Create challenge proof
 257  	proof, err := CreateChallengeProof(challenge, oldCert.Name, oldCert.CertPubkey, validUntil, ownerSigner)
 258  	if err != nil {
 259  		return nil, fmt.Errorf("failed to create challenge proof: %w", err)
 260  	}
 261  
 262  	// Create request
 263  	req := &CertificateRequest{
 264  		Name:           oldCert.Name,
 265  		CertPubkey:     oldCert.CertPubkey,
 266  		ValidFrom:      validFrom,
 267  		ValidUntil:     validUntil,
 268  		Challenge:      challenge,
 269  		ChallengeProof: proof,
 270  	}
 271  
 272  	// Collect witness signatures
 273  	var witnesses []WitnessSignature
 274  	for i, ws := range witnessSigners {
 275  		tempCert := &Certificate{
 276  			Name:       req.Name,
 277  			CertPubkey: req.CertPubkey,
 278  			ValidFrom:  req.ValidFrom,
 279  			ValidUntil: req.ValidUntil,
 280  			Challenge:  req.Challenge,
 281  		}
 282  
 283  		witness, err := RequestWitnessSignature(tempCert, ws)
 284  		if err != nil {
 285  			return nil, fmt.Errorf("failed to get witness %d signature: %w", i, err)
 286  		}
 287  
 288  		witnesses = append(witnesses, witness)
 289  	}
 290  
 291  	// Create certificate event
 292  	certEvent, err := CreateCertificateWithWitnesses(req, witnesses, oldCert.Algorithm, oldCert.Usage, ownerSigner)
 293  	if err != nil {
 294  		return nil, fmt.Errorf("failed to create certificate event: %w", err)
 295  	}
 296  
 297  	// Parse back to Certificate struct
 298  	cert, err := ParseCertificate(certEvent)
 299  	if err != nil {
 300  		return nil, fmt.Errorf("failed to parse certificate: %w", err)
 301  	}
 302  
 303  	return cert, nil
 304  }
 305  
 306  // CheckCertificateExpiry returns the time until expiration, or error if expired
 307  func CheckCertificateExpiry(cert *Certificate) (time.Duration, error) {
 308  	now := time.Now()
 309  
 310  	if now.After(cert.ValidUntil) {
 311  		return 0, fmt.Errorf("certificate expired %v ago", now.Sub(cert.ValidUntil))
 312  	}
 313  
 314  	return cert.ValidUntil.Sub(now), nil
 315  }
 316  
 317  // ShouldRenewCertificate checks if a certificate should be renewed (< 30 days until expiry)
 318  func ShouldRenewCertificate(cert *Certificate) bool {
 319  	timeUntilExpiry, err := CheckCertificateExpiry(cert)
 320  	if err != nil {
 321  		return true // Expired, definitely should renew
 322  	}
 323  
 324  	return timeUntilExpiry < 30*24*time.Hour
 325  }
 326