You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
325 lines
9.4 KiB
325 lines
9.4 KiB
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 |
|
}
|
|
|