mailout/pkg/cmd/dkimkey.go

417 lines
11 KiB
Go

package cmd
import (
"crypto/rand"
"encoding/hex"
"fmt"
"git.maurice.fr/thomas/mailout/pkg/crypto"
"git.maurice.fr/thomas/mailout/pkg/database"
"git.maurice.fr/thomas/mailout/pkg/models"
"git.maurice.fr/thomas/mailout/pkg/providers/ovh"
"git.maurice.fr/thomas/mailout/pkg/utils"
"github.com/google/uuid"
"github.com/pterm/pterm"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
flagDKIMKeyActive bool
flagSelector string
flagKeyBits int
)
var DKIMKeyCmd = &cobra.Command{
Use: "dkimkey",
Short: "manages DKIM keys",
}
var DKIMKeyAddCmd = &cobra.Command{
Use: "add [domain]",
Short: "adds a DKIM signing key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
priv, pub, err := crypto.GenerateKeyPair(flagKeyBits)
if err != nil {
panic(err)
}
selector := flagSelector
if selector == "" {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
logrus.Fatal("entropy generator is kill")
}
selector = hex.EncodeToString(b)
}
dkimkey := models.DKIMKey{
DomainName: args[0],
PublicKey: pub,
PrivateKey: priv,
Active: flagUserActive,
Selector: selector,
Record: fmt.Sprintf("%s._domainkey.%s. IN TXT \"v=DKIM1; k=rsa; p=%s\"", selector, args[0], pub),
}
err = db.Save(&dkimkey).Error
if err != nil {
logrus.WithError(err).Fatal("could not create DKIM key")
}
logrus.Infof("created DKIM key %s", dkimkey.ID)
},
}
var DKIMKeyListCmd = &cobra.Command{
Use: "list",
Short: "list DKIM keys",
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
qRes := make([]models.DKIMKey, 0)
err = db.Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not list keys")
}
tData := pterm.TableData{
{"id", "selector", "domain", "active", "created_at", "updated_at"},
}
for _, k := range qRes {
tData = append(tData, []string{k.ID.String(), k.Selector, k.DomainName, fmt.Sprintf("%v", k.Active), k.CreatedAt.String(), k.UpdatedAt.String()})
}
pterm.DefaultTable.WithHasHeader().WithData(tData).Render()
},
}
var DKIMKeyRecordCmd = &cobra.Command{
Use: "record",
Short: "gives the dns record for a key",
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
qRes := make([]models.DKIMKey, 0)
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not search keys")
}
if len(qRes) == 0 {
logrus.WithError(err).Fatal("No such key")
}
fmt.Println(qRes[0].Record)
},
}
var DKIMKeyDeleteCmd = &cobra.Command{
Use: "delete",
Short: "delete DKIM keys",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(&models.DKIMKey{ID: id}).Delete(&models.DKIMKey{}).Error
if err != nil {
logrus.WithError(err).Fatal("could not delete DKIM key")
}
logrus.Infof("deleted DKIM key %s if it existed", args[0])
},
}
var DKIMKeyActivateCmd = &cobra.Command{
Use: "activate",
Short: "activates a DKIM key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Model(&models.DKIMKey{}).Where(&models.DKIMKey{ID: id}).Update("active", true).Error
if err != nil {
logrus.WithError(err).Fatal("could not activate DKIM key")
}
logrus.Infof("activated DKIM key %s", args[0])
},
}
var DKIMKeyDeactivateCmd = &cobra.Command{
Use: "deactivate",
Short: "deactivates a DKIM key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Model(&models.DKIMKey{}).Where(&models.DKIMKey{ID: id}).Update("active", false).Error
if err != nil {
logrus.WithError(err).Fatal("could not deactivate DKIM key")
}
logrus.Infof("deactivated DKIM key %s", args[0])
},
}
var DKIMKeyPublishCmd = &cobra.Command{
Use: "publish",
Short: "publishes a DKIM key onto a DNS provider",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
qRes := make([]models.DKIMKey, 0)
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not get key")
}
if len(qRes) == 0 {
logrus.WithError(err).Fatal("No such key")
}
dkimkey := qRes[0]
logger := logrus.WithFields(logrus.Fields{
"dkimkey": dkimkey.ID,
"selector": dkimkey.Selector,
})
ovhClient, err := ovh.NewOVHProvider(cfg)
if err != nil {
logger.WithError(err).Fatal("could not get OVH DNS provider")
}
zone, err := utils.GetZone(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine zone")
}
subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine subdomain")
}
if subdomain == "" {
subdomain = zone
}
logger = logger.WithFields(logrus.Fields{
"zone": zone,
"subdomain": subdomain,
})
dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
result := make([]int, 0)
err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
if err != nil {
logger.WithError(err).Fatal("could not lookup records")
}
type createParams struct {
FieldType string `json:"fieldType"`
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
}
type updateParams struct {
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
}
if len(result) == 0 {
logger.Info("no DKIM records found, creating a new one")
c := createParams{
FieldType: "TXT",
SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
Target: fmt.Sprintf("\"v=DKIM1; k=rsa; p=%s\"", dkimkey.PublicKey),
TTL: 60,
}
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/record", zone), &c, nil)
if err != nil {
logger.WithError(err).Fatal("could not create new record")
}
logger.Info("created new DKIM record")
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
if err != nil {
logger.WithError(err).Fatal("could not refresh the zone")
}
logger.Info("refreshed zone")
} else if len(result) == 1 {
logger.Info("found one record, updating it")
u := updateParams{
SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
Target: fmt.Sprintf("\"v=DKIM1; k=foo; p=%s\"", dkimkey.PublicKey),
TTL: 60,
}
err = ovhClient.Client.Put(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), &u, nil)
if err != nil {
logger.WithError(err).Fatal("could not update record")
}
logger.Info("updated existing record")
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
if err != nil {
logger.WithError(err).Fatal("could not refresh the zone")
}
logger.Info("refreshed zone")
}
},
}
var DKIMKeyUnpublishCmd = &cobra.Command{
Use: "unpublish",
Short: "removes a DKIM key onto a DNS provider",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
db, err := database.NewDB(cfg)
if err != nil {
logrus.WithError(err).Fatal("could not connect to the database")
}
qRes := make([]models.DKIMKey, 0)
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not get key")
}
if len(qRes) == 0 {
logrus.WithError(err).Fatal("No such key")
}
dkimkey := qRes[0]
logger := logrus.WithFields(logrus.Fields{
"dkimkey": dkimkey.ID,
"selector": dkimkey.Selector,
})
ovhClient, err := ovh.NewOVHProvider(cfg)
if err != nil {
logger.WithError(err).Fatal("could not get OVH DNS provider")
}
zone, err := utils.GetZone(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine zone")
}
subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine subdomain")
}
if subdomain == "" {
subdomain = zone
}
logger = logger.WithFields(logrus.Fields{
"zone": zone,
"subdomain": subdomain,
})
dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
result := make([]int, 0)
err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
if err != nil {
logger.WithError(err).Fatal("could not lookup records")
}
if len(result) == 0 {
logger.Info("no DKIM records found, no need to do anything")
} else if len(result) == 1 {
logger.Info("found one record, deleting it")
err = ovhClient.Client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), nil)
if err != nil {
logger.WithError(err).Fatal("could not delete record")
}
logger.Info("deleted existing record")
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
if err != nil {
logger.WithError(err).Fatal("could not refresh the zone")
}
logger.Info("refreshed zone")
}
},
}
func InitDKIMKeyCmd() {
DKIMKeyCmd.AddCommand(DKIMKeyAddCmd)
DKIMKeyCmd.AddCommand(DKIMKeyListCmd)
DKIMKeyCmd.AddCommand(DKIMKeyDeleteCmd)
DKIMKeyCmd.AddCommand(DKIMKeyActivateCmd)
DKIMKeyCmd.AddCommand(DKIMKeyDeactivateCmd)
DKIMKeyCmd.AddCommand(DKIMKeyRecordCmd)
DKIMKeyCmd.AddCommand(DKIMKeyPublishCmd)
DKIMKeyCmd.AddCommand(DKIMKeyUnpublishCmd)
DKIMKeyAddCmd.PersistentFlags().BoolVarP(&flagDKIMKeyActive, "active", "a", true, "whether or not the created key is active")
DKIMKeyAddCmd.PersistentFlags().StringVarP(&flagSelector, "selector", "s", "", "force a selector for the key")
DKIMKeyAddCmd.PersistentFlags().IntVarP(&flagKeyBits, "key-bits", "k", 2048, "force a size for the key")
}