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") }