mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
261 lines
5.5 KiB
Go
261 lines
5.5 KiB
Go
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...)
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|