Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow storing keys in database #2

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 42 additions & 34 deletions internal/modify/dkim/dkim.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import (
"github.com/foxcpp/maddy/framework/exterrors"
"github.com/foxcpp/maddy/framework/log"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/table"
"github.com/foxcpp/maddy/internal/target"
"golang.org/x/net/idna"
)
Expand Down Expand Up @@ -95,27 +94,40 @@ var (
}
)

type Modifier struct {
instName string

domains []string
selector string
signers map[string]crypto.Signer
oversignHeader []string
signHeader []string
headerCanon dkim.Canonicalization
bodyCanon dkim.Canonicalization
sigExpiry time.Duration
hash crypto.Hash
multipleFromOk bool
signSubdomains bool
keyPathTemplate string
hashName string
newKeyAlgo string
sqlTable *table.SQL

log log.Logger
}
type (
Modifier struct {
instName string

domains []string
selector string
signers map[string]crypto.Signer
oversignHeader []string
signHeader []string
headerCanon dkim.Canonicalization
bodyCanon dkim.Canonicalization
sigExpiry time.Duration
hash crypto.Hash
multipleFromOk bool
signSubdomains bool
keyPathTemplate string
hashName string
newKeyAlgo string
table module.MutableTable
storeKeysInDB bool

log log.Logger
}

DKIM struct {
Domain string `json:"domain"`
PrivateKey string `json:"privateKey,omitempty"`
PublicKey string `json:"publicKey,omitempty"`
DNSName string `json:"dnsName"`
DNSValue string `json:"dnsValue"`
Expires time.Time `json:"expires,omitempty"`
pkey crypto.Signer `json:"-"`
}
)

func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
m := &Modifier{
Expand Down Expand Up @@ -147,11 +159,11 @@ func (m *Modifier) InstanceName() string {

func (m *Modifier) Init(cfg *config.Map) error {

var domainTbl module.Table
cfg.Bool("debug", true, false, &m.log.Debug)
cfg.Bool("store_keys_in_database", false, false, &m.storeKeysInDB)
cfg.StringList("domains", false, false, m.domains, &m.domains)
cfg.String("selector", false, false, m.selector, &m.selector)
cfg.Custom("domain_table", true, false, nil, modconfig.TableDirective, &domainTbl)
cfg.Custom("domain_table", true, false, nil, modconfig.TableDirective, &m.table)
cfg.String("key_path", false, false, "dkim_keys/{domain}_{selector}.key", &m.keyPathTemplate)
cfg.StringList("oversign_fields", false, false, oversignDefault, &m.oversignHeader)
cfg.StringList("sign_fields", false, false, signDefault, &m.signHeader)
Expand Down Expand Up @@ -188,15 +200,9 @@ func (m *Modifier) Init(cfg *config.Map) error {
panic("modify.dkim.Init: Hash function allowed by config matcher but not present in hashFuncs")
}

var ok bool
m.sqlTable, ok = domainTbl.(*table.SQL)
if !ok && domainTbl != nil {
return errors.New("modify.dkim: table is not SQLTable")
}

// If available, include domains from SQL table
if ok {
domains, err := m.sqlTable.Keys()
if m.table != nil {
domains, err := m.table.Keys()
if err != nil {
return err
}
Expand All @@ -206,6 +212,8 @@ func (m *Modifier) Init(cfg *config.Map) error {
}
}

storeKeysInDB := m.storeKeysInDB && m.table != nil

for _, domain := range m.domains {
if _, err := idna.ToASCII(domain); err != nil {
m.log.Printf("warning: unable to convert domain %s to A-labels form, non-EAI messages will not be signed: %v", domain, err)
Expand All @@ -214,7 +222,7 @@ func (m *Modifier) Init(cfg *config.Map) error {
keyValues := strings.NewReplacer("{domain}", domain, "{selector}", m.selector)
keyPath := keyValues.Replace(m.keyPathTemplate)

signer, newKey, err := m.loadOrGenerateKey(keyPath, m.newKeyAlgo)
signer, newKey, err := m.loadOrGenerateKey(domain, keyPath, m.newKeyAlgo, storeKeysInDB)
if err != nil {
return err
}
Expand Down Expand Up @@ -326,7 +334,7 @@ func (s *state) RewriteBody(ctx context.Context, h *textproto.Header, body buffe
}
keySigner := s.m.signers[normDomain]
if keySigner == nil {
if s.m.sqlTable == nil {
if s.m.table == nil {
s.log.Msg("no key for domain", "domain", normDomain)
return nil
}
Expand Down
135 changes: 121 additions & 14 deletions internal/modify/dkim/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
package dkim

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"

"github.com/foxcpp/maddy/framework/dns"
"golang.org/x/net/idna"
Expand All @@ -45,7 +48,8 @@ func (m *Modifier) generateKeyForDomain(domain string) (crypto.Signer, error) {
keyValues := strings.NewReplacer("{domain}", domain, "{selector}", m.selector)
keyPath := keyValues.Replace(m.keyPathTemplate)

signer, newKey, err := m.loadOrGenerateKey(keyPath, m.newKeyAlgo)
storeInDB := m.storeKeysInDB && m.table != nil
signer, newKey, err := m.loadOrGenerateKey(domain, keyPath, m.newKeyAlgo, storeInDB)
if err != nil {
return nil, err
}
Expand All @@ -67,25 +71,47 @@ func (m *Modifier) generateKeyForDomain(domain string) (crypto.Signer, error) {
m.signers[normDomain] = signer
return signer, nil
}
func (m *Modifier) loadOrGenerateKey(keyPath, newKeyAlgo string) (pkey crypto.Signer, newKey bool, err error) {
f, err := os.Open(keyPath)
if err != nil {
if os.IsNotExist(err) {
pkey, err = m.generateAndWrite(keyPath, newKeyAlgo)
func (m *Modifier) loadOrGenerateKey(domain, keyPath, newKeyAlgo string, storeInDB bool) (pkey crypto.Signer, newKey bool, err error) {
var pemBlob []byte
if storeInDB && m.table != nil {
ctx := context.Background()
keyData, ok, err := m.table.Lookup(ctx, domain)
if err != nil {
return nil, false, err
}
if !ok || keyData == "" {
pkey, err = m.generateAndWrite(domain, keyPath, newKeyAlgo, storeInDB)
return pkey, true, err
}
return nil, false, err
}
defer f.Close()
var dkimKey DKIM
if err = json.Unmarshal([]byte(keyData), &dkimKey); err != nil {
return nil, false, err
}
pemBlob = []byte(dkimKey.PrivateKey)
} else {
f, err := os.Open(keyPath)
if err != nil {
if os.IsNotExist(err) {
pkey, err = m.generateAndWrite(domain, keyPath, newKeyAlgo, storeInDB)
return pkey, true, err
}
return nil, false, err
}
defer f.Close()

pemBlob, err := io.ReadAll(f)
if err != nil {
return nil, false, err
pemBlob, err = io.ReadAll(f)
if err != nil {
return nil, false, err
}
}

block, _ := pem.Decode(pemBlob)
if block == nil {
return nil, false, fmt.Errorf("modify.dkim: %s: invalid PEM block", keyPath)
reference := keyPath
if storeInDB && m.table != nil {
reference = domain
}
return nil, false, fmt.Errorf("modify.dkim: %s: invalid PEM block", reference)
}

var key interface{}
Expand Down Expand Up @@ -125,7 +151,7 @@ func (m *Modifier) loadOrGenerateKey(keyPath, newKeyAlgo string) (pkey crypto.Si
}
}

func (m *Modifier) generateAndWrite(keyPath, newKeyAlgo string) (crypto.Signer, error) {
func (m *Modifier) generateAndWrite(domain, keyPath, newKeyAlgo string, storeInDB bool) (crypto.Signer, error) {
wrapErr := func(err error) error {
return fmt.Errorf("modify.dkim: generate %s: %w", keyPath, err)
}
Expand Down Expand Up @@ -158,6 +184,24 @@ func (m *Modifier) generateAndWrite(keyPath, newKeyAlgo string) (crypto.Signer,
return nil, wrapErr(err)
}

selector := fmt.Sprintf("%s._domainkey.%s", m.selector, domain)
dkimKey, err := keyToJSON(domain, selector, newKeyAlgo)
if err != nil {
return nil, wrapErr(err)
}

dkimKey.Expires = time.Now().Add(m.sigExpiry)

resultString, err := json.Marshal(dkimKey)
if err != nil {
return nil, wrapErr(err)
}

if storeInDB && m.table != nil {
err = m.table.SetKey(domain, string(resultString))
return pkey, err
}

// 0777 because we have public keys in here too and they don't
// need protection. Individual private key files have 0600 perms.
if err := os.MkdirAll(filepath.Dir(keyPath), 0o777); err != nil {
Expand All @@ -184,6 +228,69 @@ func (m *Modifier) generateAndWrite(keyPath, newKeyAlgo string) (crypto.Signer,
return pkey, nil
}

func keyToJSON(domain, selector, algo string) (result DKIM, err error) {
var (
dkimName = algo
pkey crypto.Signer
pubKeyBlob []byte
)

switch algo {
case "rsa4096":
dkimName = "rsa"
pkey, err = rsa.GenerateKey(rand.Reader, 4096)
case "rsa2048":
dkimName = "rsa"
pkey, err = rsa.GenerateKey(rand.Reader, 2048)
case "ed25519":
_, pkey, err = ed25519.GenerateKey(rand.Reader)
default:
err = fmt.Errorf("unknown key algorithm: %s", algo)
}
if err != nil {
return DKIM{}, err
}

keyBlob, err := x509.MarshalPKCS8PrivateKey(pkey)
if err != nil {
return DKIM{}, err
}

pubkey := pkey.Public()
switch pubkey := pubkey.(type) {
case *rsa.PublicKey:
var err error
pubKeyBlob, err = x509.MarshalPKIXPublicKey(pubkey)
if err != nil {
return DKIM{}, err
}
case ed25519.PublicKey:
pubKeyBlob = pubkey
default:
panic("modify.dkim.writeDNSRecord: unknown key algorithm")
}

pubKeyString := base64.StdEncoding.EncodeToString(pubKeyBlob)
keyRecord := fmt.Sprintf("v=DKIM1; k=%s; p=%s", dkimName, pubKeyString)

keyBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBlob})
if keyBytes == nil {
err := fmt.Errorf("failed to encode private key")
return DKIM{}, err
}

result = DKIM{
DNSValue: keyRecord,
PrivateKey: string(keyBytes),
PublicKey: pubKeyString,
pkey: pkey,
Domain: domain,
DNSName: selector,
}

return result, nil
}

func writeDNSRecord(keyPath, dkimAlgoName string, pkey crypto.Signer) (string, error) {
var (
keyBlob []byte
Expand Down
6 changes: 3 additions & 3 deletions internal/modify/dkim/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestKeyLoad_new(t *testing.T) {

dir := t.TempDir()

signer, newKey, err := m.loadOrGenerateKey(filepath.Join(dir, "testkey.key"), "ed25519")
signer, newKey, err := m.loadOrGenerateKey("", filepath.Join(dir, "testkey.key"), "ed25519", false)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -86,7 +86,7 @@ func TestKeyLoad_existing_pkcs8(t *testing.T) {
t.Fatal(err)
}

signer, newKey, err := m.loadOrGenerateKey(filepath.Join(dir, "testkey.key"), "ed25519")
signer, newKey, err := m.loadOrGenerateKey("", filepath.Join(dir, "testkey.key"), "ed25519", false)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -138,7 +138,7 @@ func TestKeyLoad_existing_pkcs1(t *testing.T) {
t.Fatal(err)
}

signer, newKey, err := m.loadOrGenerateKey(filepath.Join(dir, "testkey.key"), "rsa2048")
signer, newKey, err := m.loadOrGenerateKey("", filepath.Join(dir, "testkey.key"), "rsa2048", false)
if err != nil {
t.Fatal(err)
}
Expand Down
Loading