Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package caddytls |
| 16 | |
| 17 | import ( |
| 18 | "bytes" |
| 19 | "crypto/tls" |
| 20 | "encoding/pem" |
| 21 | "fmt" |
| 22 | "io/fs" |
| 23 | "os" |
| 24 | "path/filepath" |
| 25 | "strings" |
| 26 | |
| 27 | "github.com/caddyserver/caddy/v2" |
| 28 | ) |
| 29 | |
| 30 | func init() { |
| 31 | caddy.RegisterModule(FolderLoader{}) |
| 32 | } |
| 33 | |
| 34 | // FolderLoader loads certificates and their associated keys from disk |
| 35 | // by recursively walking the specified directories, looking for PEM |
| 36 | // files which contain both a certificate and a key. |
| 37 | type FolderLoader []string |
| 38 | |
| 39 | // CaddyModule returns the Caddy module information. |
| 40 | func (FolderLoader) CaddyModule() caddy.ModuleInfo { |
| 41 | return caddy.ModuleInfo{ |
| 42 | ID: "tls.certificates.load_folders", |
| 43 | New: func() caddy.Module { return new(FolderLoader) }, |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | // Provision implements caddy.Provisioner. |
| 48 | func (fl FolderLoader) Provision(ctx caddy.Context) error { |
| 49 | repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 50 | if !ok { |
| 51 | repl = caddy.NewReplacer() |
| 52 | } |
| 53 | for k, path := range fl { |
| 54 | fl[k] = repl.ReplaceKnown(path, "") |
| 55 | } |
| 56 | return nil |
| 57 | } |
| 58 | |
| 59 | // LoadCertificates loads all the certificates+keys in the directories |
| 60 | // listed in fl from all files ending with .pem. This method of loading |
| 61 | // certificates expects the certificate and key to be bundled into the |
| 62 | // same file. |
| 63 | func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { |
| 64 | var certs []Certificate |
| 65 | for _, dir := range fl { |
| 66 | root, err := os.OpenRoot(dir) |
| 67 | if err != nil { |
| 68 | return nil, fmt.Errorf("unable to open root directory %s: %w", dir, err) |
| 69 | } |
| 70 | err = filepath.WalkDir(dir, func(fpath string, d fs.DirEntry, err error) error { |
| 71 | if err != nil { |
| 72 | return fmt.Errorf("unable to traverse into path: %s", fpath) |
| 73 | } |
| 74 | if d.IsDir() { |
| 75 | return nil |
| 76 | } |
| 77 | if !strings.HasSuffix(strings.ToLower(d.Name()), ".pem") { |
| 78 | return nil |
| 79 | } |
| 80 | |
| 81 | rel, err := filepath.Rel(dir, fpath) |
| 82 | if err != nil { |
| 83 | return fmt.Errorf("unable to get relative path for %s: %w", fpath, err) |
| 84 | } |
| 85 | |
| 86 | bundle, err := root.ReadFile(rel) |
| 87 | if err != nil { |
| 88 | return err |
| 89 | } |
| 90 | cert, err := tlsCertFromCertAndKeyPEMBundle(bundle) |
| 91 | if err != nil { |
| 92 | return fmt.Errorf("%s: %w", fpath, err) |
| 93 | } |
| 94 | |
| 95 | certs = append(certs, Certificate{Certificate: cert}) |
| 96 | return nil |
| 97 | }) |
| 98 | _ = root.Close() |
| 99 | if err != nil { |
| 100 | return nil, fmt.Errorf("walking certificates directory %s: %w", dir, err) |
| 101 | } |
| 102 | } |
| 103 | return certs, nil |
| 104 | } |
| 105 | |
| 106 | func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) { |
| 107 | certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer) |
| 108 | var foundKey bool // use only the first key in the file |
| 109 | |
| 110 | for { |
| 111 | // Decode next block so we can see what type it is |
| 112 | var derBlock *pem.Block |
| 113 | derBlock, bundle = pem.Decode(bundle) |
| 114 | if derBlock == nil { |
| 115 | break |
| 116 | } |
| 117 | |
| 118 | if derBlock.Type == "CERTIFICATE" { |
| 119 | // Re-encode certificate as PEM, appending to certificate chain |
| 120 | if err := pem.Encode(certBuilder, derBlock); err != nil { |
| 121 | return tls.Certificate{}, err |
| 122 | } |
| 123 | } else if derBlock.Type == "EC PARAMETERS" { |
| 124 | // EC keys generated from openssl can be composed of two blocks: |
| 125 | // parameters and key (parameter block should come first) |
| 126 | if !foundKey { |
| 127 | // Encode parameters |
| 128 | if err := pem.Encode(keyBuilder, derBlock); err != nil { |
| 129 | return tls.Certificate{}, err |
| 130 | } |
| 131 | |
| 132 | // Key must immediately follow |
| 133 | derBlock, bundle = pem.Decode(bundle) |
| 134 | if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" { |
| 135 | return tls.Certificate{}, fmt.Errorf("expected elliptic private key to immediately follow EC parameters") |
| 136 | } |
| 137 | if err := pem.Encode(keyBuilder, derBlock); err != nil { |
| 138 | return tls.Certificate{}, err |
| 139 | } |
| 140 | foundKey = true |
| 141 | } |
| 142 | } else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { |
| 143 | // RSA key |
| 144 | if !foundKey { |
| 145 | if err := pem.Encode(keyBuilder, derBlock); err != nil { |
| 146 | return tls.Certificate{}, err |
| 147 | } |
| 148 | foundKey = true |
| 149 | } |
| 150 | } else { |
| 151 | return tls.Certificate{}, fmt.Errorf("unrecognized PEM block type: %s", derBlock.Type) |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes() |
| 156 | if len(certPEMBytes) == 0 { |
| 157 | return tls.Certificate{}, fmt.Errorf("failed to parse PEM data") |
| 158 | } |
| 159 | if len(keyPEMBytes) == 0 { |
| 160 | return tls.Certificate{}, fmt.Errorf("no private key block found") |
| 161 | } |
| 162 | |
| 163 | // if the start of the key file looks like an encrypted private key, |
| 164 | // reject it with a helpful error message |
| 165 | if strings.HasPrefix(string(keyPEMBytes[:40]), "ENCRYPTED") { |
| 166 | return tls.Certificate{}, fmt.Errorf("encrypted private keys are not supported; please decrypt the key first") |
| 167 | } |
| 168 | |
| 169 | cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes) |
| 170 | if err != nil { |
| 171 | return tls.Certificate{}, fmt.Errorf("making X509 key pair: %v", err) |
| 172 | } |
| 173 | |
| 174 | return cert, nil |
| 175 | } |
| 176 | |
| 177 | var ( |
| 178 | _ CertificateLoader = FolderLoader(nil) |
| 179 | _ caddy.Provisioner = FolderLoader(nil) |
| 180 | ) |