326 lines
9.4 KiB
Go
326 lines
9.4 KiB
Go
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
|
|
}
|