package main import ( "bytes" "context" "crypto" "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "io" "net/mail" "os" "path" "path/filepath" "strings" "time" "blitiri.com.ar/go/chasquid/internal/dkim" "blitiri.com.ar/go/chasquid/internal/envelope" "blitiri.com.ar/go/chasquid/internal/normalize" ) func dkimSign() { domain := args["$2"] selector := args["$3"] keyPath := args["$4"] msg, err := io.ReadAll(os.Stdin) if err != nil { Fatalf("%v", err) } msg = normalize.ToCRLF(msg) if domain == "" { domain = getDomainFromMsg(msg) } if selector == "" { selector = findSelectorForDomain(domain) } if keyPath == "" { keyPath = keyPathFor(domain, selector) } signer := &dkim.Signer{ Domain: domain, Selector: selector, Signer: loadPrivateKey(keyPath), } ctx := context.Background() if _, verbose := args["-v"]; verbose { ctx = dkim.WithTraceFunc(ctx, func(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format+"\n", args...) }) } header, err := signer.Sign(ctx, string(msg)) if err != nil { Fatalf("Error signing message: %v", err) } fmt.Printf("DKIM-Signature: %s\r\n", strings.ReplaceAll(header, "\r\n", "\r\n\t")) } func dkimVerify() { msg, err := io.ReadAll(os.Stdin) if err != nil { Fatalf("%v", err) } msg = normalize.ToCRLF(msg) ctx := context.Background() if _, verbose := args["-v"]; verbose { ctx = dkim.WithTraceFunc(ctx, func(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format+"\n", args...) }) } if txt, ok := args["--txt"]; ok { ctx = dkim.WithLookupTXTFunc(ctx, func(ctx context.Context, domain string) ([]string, error) { return []string{txt}, nil }) } results, err := dkim.VerifyMessage(ctx, string(msg)) if err != nil { Fatalf("Error verifying message: %v", err) } hostname, _ := os.Hostname() ar := "Authentication-Results: " + hostname + "\r\n\t" ar += strings.ReplaceAll( results.AuthenticationResults(), "\r\n", "\r\n\t") fmt.Println(ar) } func dkimDNS() { domain := args["$2"] selector := args["$3"] keyPath := args["$4"] if domain == "" { Fatalf("Error: missing domain parameter") } if selector == "" { selector = findSelectorForDomain(domain) } if keyPath == "" { keyPath = keyPathFor(domain, selector) } fmt.Println(dnsRecordFor(domain, selector, loadPrivateKey(keyPath))) } func dnsRecordFor(domain, selector string, private crypto.Signer) string { public := private.Public() var err error algoStr := "" pubBytes := []byte{} switch private.(type) { case *rsa.PrivateKey: algoStr = "rsa" pubBytes, err = x509.MarshalPKIXPublicKey(public) case ed25519.PrivateKey: algoStr = "ed25519" pubBytes = public.(ed25519.PublicKey) } if err != nil { Fatalf("Error marshaling public key: %v", err) } return fmt.Sprintf( "%s._domainkey.%s\tTXT\t\"v=DKIM1; k=%s; p=%s\"", selector, domain, algoStr, base64.StdEncoding.EncodeToString(pubBytes)) } func dkimKeygen() { domain := args["$2"] selector := args["$3"] keyPath := args["$4"] algo := args["--algo"] if domain == "" { Fatalf("Error: missing domain parameter") } if selector == "" { selector = time.Now().UTC().Format("20060102") } if keyPath == "" { keyPath = keyPathFor(domain, selector) } if _, err := os.Stat(keyPath); !os.IsNotExist(err) { Fatalf("Error: key already exists at %q", keyPath) } var private crypto.Signer var err error switch algo { case "", "rsa3072": private, err = rsa.GenerateKey(rand.Reader, 3072) case "rsa4096": private, err = rsa.GenerateKey(rand.Reader, 4096) case "ed25519": _, private, err = ed25519.GenerateKey(rand.Reader) default: Fatalf("Error: unsupported algorithm %q", algo) } if err != nil { Fatalf("Error generating key: %v", err) } privB, err := x509.MarshalPKCS8PrivateKey(private) if err != nil { Fatalf("Error marshaling private key: %v", err) } f, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660) if err != nil { Fatalf("Error creating key file %q: %v", keyPath, err) } block := &pem.Block{ Type: "PRIVATE KEY", Bytes: privB, } if err := pem.Encode(f, block); err != nil { Fatalf("Error PEM-encoding key: %v", err) } f.Close() fmt.Printf("Key written to %q\n\n", keyPath) fmt.Println(dnsRecordFor(domain, selector, private)) } func keyPathFor(domain, selector string) string { return path.Clean(fmt.Sprintf("%s/domains/%s/dkim:%s.pem", configDir, domain, selector)) } func getDomainFromMsg(msg []byte) string { m, err := mail.ReadMessage(bytes.NewReader(msg)) if err != nil { Fatalf("Error parsing message: %v", err) } addr, err := mail.ParseAddress(m.Header.Get("From")) if err != nil { Fatalf("Error parsing From: header: %v", err) } return envelope.DomainOf(addr.Address) } func findSelectorForDomain(domain string) string { glob := path.Clean(configDir + "/domains/" + domain + "/dkim:*.pem") ms, err := filepath.Glob(glob) if err != nil { Fatalf("Error finding DKIM keys: %v", err) } for _, m := range ms { base := filepath.Base(m) selector := strings.TrimPrefix(base, "dkim:") selector = strings.TrimSuffix(selector, ".pem") return selector } Fatalf("No DKIM keys found in %q", glob) return "" } func loadPrivateKey(path string) crypto.Signer { key, err := os.ReadFile(path) if err != nil { Fatalf("Error reading private key from %q: %v", path, err) } block, _ := pem.Decode(key) if block == nil { Fatalf("Error decoding PEM block") } switch strings.ToUpper(block.Type) { case "PRIVATE KEY": k, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { Fatalf("Error parsing private key: %v", err) } return k.(crypto.Signer) default: Fatalf("Unsupported key type: %s", block.Type) return nil } }