package hammerhead import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "net/http" "regexp" "sort" "strings" "time" "git.veillax.com/Veillax/reef/model" ) // DKIM: Hammerhead makes a signing key for a sending domain, gives the edge the // private half, and shows you the public half as the DNS record to publish. The // edge signs mail on its way out; receiving servers fetch the record to check it. var selectorRE = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,40}[a-z0-9])?$`) func (k *dkimView) record() string { return "v=DKIM1; k=rsa; p=" + k.PublicKey } type dkimView struct { ID string `json:"id"` Domain string `json:"domain"` Selector string `json:"selector"` PublicKey string `json:"publicKey"` Created time.Time `json:"created"` RecordName string `json:"recordName"` Record string `json:"record"` Status string `json:"status"` // published, missing, mismatch or unknown } func (s *Server) listDKIM(w http.ResponseWriter, r *http.Request, c *orgCtx) { s.mu.Lock() defer s.mu.Unlock() out := []dkimView{} for _, k := range s.st.DKIM { if k.OrgID != c.Org.ID { continue } v := dkimView{ID: k.ID, Domain: k.Domain, Selector: k.Selector, PublicKey: k.PublicKey, Created: k.Created, Status: "unknown"} v.RecordName, v.Record = k.Selector+"._domainkey."+k.Domain, v.record() for _, x := range s.st.MailEgress { if x.OrgID != c.Org.ID { continue } switch st := s.mailChecks[x.ID].DKIM[k.ID]; { case st == "published": v.Status = "published" case st != "" && v.Status != "published": v.Status = st } } out = append(out, v) } sort.Slice(out, func(i, j int) bool { return out[i].Domain+out[i].Selector < out[j].Domain+out[j].Selector }) writeJSON(w, 200, out) } func (s *Server) createDKIM(w http.ResponseWriter, r *http.Request, c *orgCtx) { var in struct{ Domain, Selector string } if !decode(w, r, &in) { return } in.Domain = strings.ToLower(strings.TrimSpace(in.Domain)) in.Selector = strings.ToLower(strings.TrimSpace(in.Selector)) if in.Selector == "" { in.Selector = "hh" + time.Now().Format("200601") } if !validHostname(in.Domain) || strings.HasPrefix(in.Domain, "*.") { fail(w, 400, errors.New("enter the domain you send mail from, like example.com")) return } if !selectorRE.MatchString(in.Selector) { fail(w, 400, errors.New("a selector is letters, digits and dashes, like mail2026")) return } key, err := rsa.GenerateKey(rand.Reader, 2048) // RSA: every receiving server can verify it if err != nil { fail(w, 500, err) return } priv, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { fail(w, 500, err) return } pub, err := x509.MarshalPKIXPublicKey(&key.PublicKey) if err != nil { fail(w, 500, err) return } s.mu.Lock() defer s.mu.Unlock() for _, k := range s.st.DKIM { if k.Domain == in.Domain && k.Selector == in.Selector { fail(w, 400, errors.New("you already have a key with that selector for this domain; pick a new selector to rotate")) return } } k := &model.DKIMKey{ID: newID(), OrgID: c.Org.ID, Domain: in.Domain, Selector: in.Selector, Created: time.Now(), PrivateKey: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: priv})), PublicKey: base64.StdEncoding.EncodeToString(pub)} s.st.DKIM = append(s.st.DKIM, k) s.bump() // edges need the new signer s.audit(c.Org.ID, c.Actor, "dkim.create", in.Selector+"._domainkey."+in.Domain) v := dkimView{ID: k.ID, Domain: k.Domain, Selector: k.Selector, PublicKey: k.PublicKey, Created: k.Created, Status: "unknown"} v.RecordName, v.Record = k.Selector+"._domainkey."+k.Domain, v.record() writeJSON(w, 200, v) } func (s *Server) deleteDKIM(w http.ResponseWriter, r *http.Request, c *orgCtx) { id := r.PathValue("id") s.mu.Lock() defer s.mu.Unlock() var gone *model.DKIMKey for _, k := range s.st.DKIM { if k.ID == id && k.OrgID == c.Org.ID { gone = k } } if gone == nil { fail(w, 404, errors.New("no such key")) return } s.st.DKIM = filter(s.st.DKIM, func(k *model.DKIMKey) bool { return k.ID != id }) s.bump() s.audit(c.Org.ID, c.Actor, "dkim.delete", gone.Selector+"._domainkey."+gone.Domain) ok(w) } // signersLocked lists the signing keys an org's mail egress may use. Caller holds s.mu. func (s *Server) signersLocked(orgID string) []model.DKIMSigner { out := []model.DKIMSigner{} for _, k := range s.st.DKIM { if k.OrgID == orgID { out = append(out, model.DKIMSigner{KeyID: k.ID, Domain: k.Domain, Selector: k.Selector, PrivateKey: k.PrivateKey, PublicKey: k.PublicKey}) } } return out }