1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package dkim2021import (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)3536func (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, err42 }43 return nil, false, err44 }45 defer f.Close()4647 pemBlob, err := io.ReadAll(f)48 if err != nil {49 return nil, false, err50 }5152 block, _ := pem.Decode(pemBlob)53 if block == nil {54 return nil, false, fmt.Errorf("modify.dkim: %s: invalid PEM block", keyPath)55 }5657 var key interface{}58 switch block.Type {59 case "PRIVATE KEY": // RFC 5208 aka PKCS #860 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 #165 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 591570 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 }7778 switch key := key.(type) {79 case *rsa.PrivateKey:80 if err := key.Validate(); err != nil {81 return nil, false, err82 }83 key.Precompute()84 return key, false, nil85 case ed25519.PrivateKey:86 return key, false, nil87 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}9394func (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 }9899 m.log.Printf("generating a new %s keypair...", newKeyAlgo)100101 var (102 pkey crypto.Signer103 dkimName = newKeyAlgo104 err error105 )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 }121122 keyBlob, err := x509.MarshalPKCS8PrivateKey(pkey)123 if err != nil {124 return nil, wrapErr(err)125 }126127 // 0777 because we have public keys in here too and they don't128 // 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 }132133 _, err = writeDNSRecord(keyPath, dkimName, pkey)134 if err != nil {135 return nil, wrapErr(err)136 }137138 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 }142143 if err := pem.Encode(f, &pem.Block{144 Type: "PRIVATE KEY",145 Bytes: keyBlob,146 }); err != nil {147 return nil, wrapErr(err)148 }149150 return pkey, nil151}152153func writeDNSRecord(keyPath, dkimAlgoName string, pkey crypto.Signer) (string, error) {154 var (155 keyBlob []byte156 pubkey = pkey.Public()157 )158 switch pubkey := pubkey.(type) {159 case *rsa.PublicKey:160 var err error161 keyBlob, err = x509.MarshalPKIXPublicKey(pubkey)162 if err != nil {163 return "", err164 }165 case ed25519.PublicKey:166 keyBlob = pubkey167 default:168 panic("modify.dkim.writeDNSRecord: unknown key algorithm")169 }170171 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 "", err178 }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 "", err182 }183 return dnsPath, nil184}