maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package dkim
 20
 21import (
 22	"crypto"
 23	"crypto/ecdsa"
 24	"crypto/ed25519"
 25	"crypto/rand"
 26	"crypto/rsa"
 27	"crypto/x509"
 28	"encoding/base64"
 29	"encoding/pem"
 30	"fmt"
 31	"io"
 32	"os"
 33	"path/filepath"
 34)
 35
 36func (m *Modifier) loadOrGenerateKey(keyPath, newKeyAlgo string) (pkey crypto.Signer, newKey bool, err error) {
 37	f, err := os.Open(keyPath)
 38	if err != nil {
 39		if os.IsNotExist(err) {
 40			pkey, err = m.generateAndWrite(keyPath, newKeyAlgo)
 41			return pkey, true, err
 42		}
 43		return nil, false, err
 44	}
 45	defer f.Close()
 46
 47	pemBlob, err := io.ReadAll(f)
 48	if err != nil {
 49		return nil, false, err
 50	}
 51
 52	block, _ := pem.Decode(pemBlob)
 53	if block == nil {
 54		return nil, false, fmt.Errorf("modify.dkim: %s: invalid PEM block", keyPath)
 55	}
 56
 57	var key interface{}
 58	switch block.Type {
 59	case "PRIVATE KEY": // RFC 5208 aka PKCS #8
 60		key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
 61		if err != nil {
 62			return nil, false, fmt.Errorf("modify.dkim: %s: %w", keyPath, err)
 63		}
 64	case "RSA PRIVATE KEY": // RFC 3447 aka PKCS #1
 65		key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
 66		if err != nil {
 67			return nil, false, fmt.Errorf("modify.dkim: %s: %w", keyPath, err)
 68		}
 69	case "EC PRIVATE KEY": // RFC 5915
 70		key, err = x509.ParseECPrivateKey(block.Bytes)
 71		if err != nil {
 72			return nil, false, fmt.Errorf("modify.dkim: %s: %w", keyPath, err)
 73		}
 74	default:
 75		return nil, false, fmt.Errorf("modify.dkim: %s: not a private key or unsupported format", keyPath)
 76	}
 77
 78	switch key := key.(type) {
 79	case *rsa.PrivateKey:
 80		if err := key.Validate(); err != nil {
 81			return nil, false, err
 82		}
 83		key.Precompute()
 84		return key, false, nil
 85	case ed25519.PrivateKey:
 86		return key, false, nil
 87	case *ecdsa.PublicKey:
 88		return nil, false, fmt.Errorf("modify.dkim: %s: ECDSA keys are not supported", keyPath)
 89	default:
 90		return nil, false, fmt.Errorf("modify.dkim: %s: unknown key type: %T", keyPath, key)
 91	}
 92}
 93
 94func (m *Modifier) generateAndWrite(keyPath, newKeyAlgo string) (crypto.Signer, error) {
 95	wrapErr := func(err error) error {
 96		return fmt.Errorf("modify.dkim: generate %s: %w", keyPath, err)
 97	}
 98
 99	m.log.Printf("generating a new %s keypair...", newKeyAlgo)
100
101	var (
102		pkey     crypto.Signer
103		dkimName = newKeyAlgo
104		err      error
105	)
106	switch newKeyAlgo {
107	case "rsa4096":
108		dkimName = "rsa"
109		pkey, err = rsa.GenerateKey(rand.Reader, 4096)
110	case "rsa2048":
111		dkimName = "rsa"
112		pkey, err = rsa.GenerateKey(rand.Reader, 2048)
113	case "ed25519":
114		_, pkey, err = ed25519.GenerateKey(rand.Reader)
115	default:
116		err = fmt.Errorf("unknown key algorithm: %s", newKeyAlgo)
117	}
118	if err != nil {
119		return nil, wrapErr(err)
120	}
121
122	keyBlob, err := x509.MarshalPKCS8PrivateKey(pkey)
123	if err != nil {
124		return nil, wrapErr(err)
125	}
126
127	// 0777 because we have public keys in here too and they don't
128	// need protection. Individual private key files have 0600 perms.
129	if err := os.MkdirAll(filepath.Dir(keyPath), 0o777); err != nil {
130		return nil, wrapErr(err)
131	}
132
133	_, err = writeDNSRecord(keyPath, dkimName, pkey)
134	if err != nil {
135		return nil, wrapErr(err)
136	}
137
138	f, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
139	if err != nil {
140		return nil, wrapErr(err)
141	}
142
143	if err := pem.Encode(f, &pem.Block{
144		Type:  "PRIVATE KEY",
145		Bytes: keyBlob,
146	}); err != nil {
147		return nil, wrapErr(err)
148	}
149
150	return pkey, nil
151}
152
153func writeDNSRecord(keyPath, dkimAlgoName string, pkey crypto.Signer) (string, error) {
154	var (
155		keyBlob []byte
156		pubkey  = pkey.Public()
157	)
158	switch pubkey := pubkey.(type) {
159	case *rsa.PublicKey:
160		var err error
161		keyBlob, err = x509.MarshalPKIXPublicKey(pubkey)
162		if err != nil {
163			return "", err
164		}
165	case ed25519.PublicKey:
166		keyBlob = pubkey
167	default:
168		panic("modify.dkim.writeDNSRecord: unknown key algorithm")
169	}
170
171	dnsPath := keyPath + ".dns"
172	if filepath.Ext(keyPath) == ".key" {
173		dnsPath = keyPath[:len(keyPath)-4] + ".dns"
174	}
175	dnsF, err := os.Create(dnsPath)
176	if err != nil {
177		return "", err
178	}
179	keyRecord := fmt.Sprintf("v=DKIM1; k=%s; p=%s", dkimAlgoName, base64.StdEncoding.EncodeToString(keyBlob))
180	if _, err := io.WriteString(dnsF, keyRecord); err != nil {
181		return "", err
182	}
183	return dnsPath, nil
184}