├── LICENSE
├── Readme.md
└── src
└── certshop.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 VARASYS Limited
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # certshop
2 |
3 | Certshop is the easy way to create server certificates for web servers; and so much more...
4 |
5 | Certshop is a standalone application for Mac, Linux and Windows to generate Private Key Infrastructure (PKI) Certificate Authorities (CA), Intermediate Certificate Authorities (ICA), and x.509 v3 certificates for TLS key exchanges and digital signatures and the certificate portion of OpenVPN config files.
6 |
7 | All private keys use Elliptic Curve secp384r1, and signatures are ECDSA Signature with SHA-384, which is believed to follow current best practices. Certshop is written in go and uses go's standard cryptography libraries.
8 |
9 | Binaries for Mac, Linux and Windows are available for download at https://github.com/varasys/certshop/releases.
10 |
11 | ## Quick Start
12 | To make a Certificate Authority and a server certificate:
13 |
14 | ```bash
15 | certshop ca -dn="/CN=My CA/O=My Organization/OU=My Organizational Unit" ca
16 | certshop server -dn="/CN=host.domain.com" ca/host_domain_com
17 | ```
18 |
19 | The "ca" private key and certificate will be in the "./ca" folder, and the server private key and certificate will be in the "./ca/host_domain_com" folder (refer to the "export" command below for other options). Every folder will also include a file called "ca.pem" with the ca certificate (without private key).
20 |
21 | The server Distinguished Name ("-dn" flag) first inherits the DN from the ca, and then overwrites any values specifically provided in the server "-dn" flag, so the final DN for the server is "/CN=host.domain.com/O=My Organization/OU=My Organizational Unit".
22 |
23 | Inheritance can be blocked by leaving the field empty. For instance -dn="/CN=host.domain.com/OU=" will prevent the OU from being inherited.
24 |
25 | To make additional server or client certificates continue to run the `certshop server` command or `certshop client` command once for each certificate with the required DN information.
26 |
27 | ```bash
28 | # create a second server cert
29 | certshop server -dn="/CN=host2.domain.com" ca/host2_domain_com
30 | # create a client cert
31 | certshop client -dn="/CN=name of client" ca/name_of_client
32 | ```
33 |
34 | Subject Alternative Names (SAN) may be provided with the "-san" flag. By default the SAN includes "127.0.0.1" and "localhost", but these defaults won't be included if the "-san" flag is explicitly supplied, so they should be included in the "-san" flag as shown below if needed.
35 |
36 | ```bash
37 | # create a server cert for my.domain.com and my.domain.org.
38 | certshop server -dn="/CN=my.domain.com" -san="127.0.0.1,localhost,my.domain.org" ca/my_domain_com
39 | ```
40 |
41 | ### Intermediate Certificate Authorities
42 | Intermediate Certificate Authorities are created with the "ica" command.
43 |
44 | Updating the first example to include an ICA is shown below.
45 |
46 | ```bash
47 | certshop ca -dn="/CN=My CA/O=My Organization/OU=My Organizational Unit" ca
48 | certshop ica -dn="/CN=My ICA" ca/ica
49 | certshop server -dn="/CN=host.domain.com" ca/ica/host_domain_com
50 | ```
51 |
52 | ## Detailed Instructions
53 |
54 | The full form of the certshop command is:
55 |
56 | ```bash
57 | certshop command [flags] [path]
58 | ```
59 |
60 | Where:
61 |
62 | - **command** is one of the following:
63 | - **ca**: create a certificate authority
64 | - **ica**: create an intermediate certificate authority
65 | - **server**: create a server certificate
66 | - **client**: create a client certificate
67 | - **signature**: create a certificate for digital signatures (ie. for signing pdf files, etc.)
68 | - **export**: export certificates in various formats to stdout as a compressed tarball (.tgz format)
69 | - Flags for the **ca** and **ica** command are:
70 | - **-dn**: the Distinguished Name of the certificate (before considering inheritance from the parent ca)
71 | - **-maxPathLength**: maximum number of subordinate Intermediate Certificate Authorities (ICA) (default = 0)
72 | - **-validity**: number of days the certificate is valid starting from the current time (ca default = 10 years, ica default = 5 years)
73 | - **-overwrite**: whether or not to overwrite existing files when creating certificates (default = false)
74 | - Flags for the **server**, **client**, and **signature** command are:
75 | - **-dn**: the Distinguished Name of the certificate (before considering inheritance from the parent ca)
76 | - **-san**: comma separated list of Subject Alternate Names
77 | - **-validity**: number of days the certificate is valid starting from the current time (default = 370 days)
78 | - **-overwrite**: whether or not to overwrite existing files when creating certificates (default = false)
79 | - Flags for the **export** command are:
80 | - **-crt**: include the certificate (including CA cert and all ICA certs) in PEM format (default = true)
81 | - **-key**: include the private key in PEM format (default = true)
82 | - **-ca**: include the CA certificate (default = true)
83 | - **-p12**: include the certificate and private key together in a password protected pkcs12 file (default = false)
84 | - **-password**: password for the the pkcs12 private key (only used when -p12 = true)
85 | - **-openvpn**: concat the certificate, private key and ca certificate into a text file that can be appended to the end of an openvpn configuration file to embed the certificates directly in the configuration file (default = false)
86 |
87 | ### Distinguished Names
88 |
89 | The Distinguished Name (DN) can be set with the "-dn" flag which expects a quoted list of key value pairs in the form `/key=value` where the keys listed below are valid. Note that "/" is included in the beginning and as a separator between key value pairs.
90 |
91 | - **CN** - Common Name (never inherited)
92 | - **C** - Country
93 | - **L** - Locality
94 | - **ST** - State or Province
95 | - **O** - Organization
96 | - **OU** - Organizational Unit
97 |
98 | The Distinguished Name for a certificate is first inherited from the certificate authority which will sign the certificate, and then modified by the "-dn" flag of the certificate being generated. Inheritance of a value can be masked by leaving the value empty.
99 |
100 | ```bash
101 | certshop ca -dn="/CN=My CA/O=My Organization/OU=My Organizational Unit"
102 | certshop server -dn="/CN=host.domain.com/OU="
103 | ```
104 |
105 | In the example above, the final distinguished name for the server will be "/CA=host.domain.com/O=My Organization". Note that "O" was inherited from the ca, and that since the server -dn flag includes "OU=" (ie. an empty value) the "OU" value is not inherited and left blank.
106 |
107 | ### Certificate Path
108 |
109 | The **path** is an absolute path or relative path from the current working folder to the folder to save the certificate, and the folders will be created when the certificate is generated if they don't already exist. CAs will use self-signed certificates and everything else will be signed by the certificate immediately above it in the path. The following default paths are defined for convenience when setting up a simple infrastructure with no ICA, but it is recommended to always specify a path.
110 |
111 | - **ca**: ca
112 | - **ica**: ca/ica
113 | - **server**: ca/server
114 | - **client**: ca/client
115 | - **signature**: ca/sign
116 |
117 | ## Using Intermediate Certificate Authorities
118 | The "-maxPathLength" flag for a certificate authority or intermediate certificate authority limits the depth of subordinate intermediate certificate authorities that can sign certificates. By default "-maxPathLength=0" (so the CA can only sign end certificates and not any ICAs). The example below demonstrates the significance of maxPathLength.
119 |
120 | ```bash
121 | certshop ca -maxPathLength=2 ca # there can't be more than 2 ICAs under this cert
122 | certshop ica ca/ica # no problem with this ica
123 | certshop ica ca/ica/ica2 # no problem with this ica
124 | certshop ica ca/ica/ica2/ica3 # this will fail because it is nested too deep
125 | ```
126 |
127 | ## Exporting Files
128 | One folder is created for each certificate key pair and includes the following files (where *name* is the last part of the path used to create the certificate):
129 |
130 | - **name.crt**: the certificate file in PEM format
131 | - **name.key**: the key file in PEM format
132 | - **ca.pem**: the top level ca certificate in PEM format
133 |
134 | If the certificate is a CA or ICA then it may have further sub-folders for each of the certificates it has signed.
135 |
136 | The **ca.pem** file is included because if somebody else is an administrator of an ICA, you could send them the ICA folder for the certificates they are administering and they would be able to use the certshop program to create certificates from that ICA without needing the top level CA key.
137 |
138 | Although all certificates and keys are stored in a flat file structure and you can copy the PEM format certificates and keys directly out of the file structure, the `export` command is provided for convenience, to provide conversion to pkcs12 format, and to provide a openvpn config snippet which can be used to embed the certificates and private key directly in an OpenVPN config file.
139 |
140 | Refer to the "Flags for the **export** command" section above for a description of all of the export options. By default the flags are: `-crt=true -key=true -ca=true -p12=false -openvpn=false`.
141 |
142 | The reason the **export** command writes to stdout instead of saving to a file is to make it easier to remotely connect to a server and create and download new certificates. Assuming you can connect to the computer where the certificates are stored, the following command would connect remotely, create a new server certificate, and download it to the local machine.
143 |
144 | ```bash
145 | ssh certserver cd /path/to/ca/folder/parent; certshop create ca/server; certshop export ca/server \
146 | | tar -zxvC /path/to/cert/destination/folder
147 | ```
148 |
149 | When using automatic provisioning (ie. when creating a cluster), if the provisioner can connect to the machine being provisioned via ssh, and specifies the "-A" ssh flag (ForwardAgent) when connecting, and ssh-agent is running on the provisioner (start it with `ssh-agent && ssh-add`), then the *machine being provisioned* will be able to use the ssh keys from the user account on the provisioning server to connect to other machines (ie. to run `certshop` on a remote machine) even though the ssh keys aren't physically located on the machine being provisioned.
150 |
151 | When the "-crt" flag is specified the certificate will be included twice, once with the name according to the path and ".crt" extension, and the other file will be named "cert.pem". Other than the name, the two files are identical. The reason for including two files is because some users will want to a descriptive name, and other users will want a fixed unchanging name (especially when automatically provisioning new servers). The "-key" flag works similar except one file with the name according to the path with a ".key" extension, and the other file will be named "key.pem".
152 |
153 | This design was motivated by a need to provide cluster node certificates for kubernetes clusters. With this design, each node can securely connect to a "certificate server" via ssh to run the certshop program and create and download its own certificates.
154 |
155 | Note that the `cd` command above uses the folder *above* the top level ca certificate folder (ie. the folder that the ca folder is located in).
156 |
157 | The following example shows how to export p12 format with the "-p12" and "-password" flags, and also how to pipe (ie. save) the results of the export command to a local ".tgz" file.
158 |
159 | ```bash
160 | certshop export -crt=false -key=false -ca=false -p12=true -password="secret" ca > ca.tgz
161 | ```
162 |
163 | ## Issues
164 |
165 | 1. CRL and OCSP revocation is not currently implemented, but probably could be if there is demand for it.
166 | 2. OpenSSL is called externally when exporting a certificate/key pair in .p12 format, so openssl must be installed and included in the current PATH (you can check this by confirming the command `which openssl` returns a valid path). Otherwise there are no other external dependencies.
167 |
168 | ## Contribution
169 |
170 | Feel free to contribute, ask questions or provide advice at https://github.com/varasys/certshop.
171 |
--------------------------------------------------------------------------------
/src/certshop.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "compress/gzip"
7 | "crypto/ecdsa"
8 | "crypto/elliptic"
9 | "crypto/rand"
10 | "crypto/x509"
11 | "crypto/x509/pkix"
12 | "encoding/pem"
13 | "flag"
14 | "io"
15 | "io/ioutil"
16 | "log"
17 | "math/big"
18 | "net"
19 | "net/mail"
20 | "os"
21 | "os/exec"
22 | "path/filepath"
23 | "strings"
24 | "text/template"
25 | "time"
26 | )
27 |
28 | var infoLog = log.New(os.Stderr, "", 0)
29 | var errorLog = log.New(os.Stderr, "ERROR: ", log.Lshortfile)
30 |
31 | var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128)
32 | var privatePerms os.FileMode = 0600
33 | var publicPerms os.FileMode = 0644
34 |
35 | func main() {
36 | var command string
37 | if len(os.Args) < 1 {
38 | command = ""
39 | } else {
40 | command = os.Args[1]
41 | }
42 | switch command {
43 | case "ca":
44 | createCA(os.Args[2:], "ca", "/CN=certstore-ca", 10*365+5)
45 | case "ica":
46 | createCA(os.Args[2:], "ca/ica", "/CN=certstore-ica", 5*365+5)
47 | case "server":
48 | createCertificate(os.Args[2:], "ca/server", "/CN=server", "localhost,127.0.0.1", 365+5,
49 | x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment,
50 | []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
51 | case "client":
52 | createCertificate(os.Args[2:], "ca/client", "/CN=client", "", 365+5,
53 | x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment,
54 | []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
55 | case "signature":
56 | createCertificate(os.Args[2:], "ca/sign", "/CN=sign", "", 365+5,
57 | x509.KeyUsageDigitalSignature, nil)
58 | case "export":
59 | exportCertificate(os.Args[2:])
60 | default:
61 | infoLog.Println("Usage: certshop ca | ica | server | client | signature | export")
62 | }
63 | }
64 |
65 | func createCA(args []string, path string, defaultDn string, defaultValidity int) {
66 | fs := flag.NewFlagSet("ca", flag.PanicOnError)
67 | dn := fs.String("dn", defaultDn, "certificate subject")
68 | maxPathLength := fs.Int("maxPathLength", 0, "max path length")
69 | validity := fs.Int("validity", defaultValidity, "ca validity in days")
70 | overwrite := fs.Bool("overwrite", false, "overwrite any existing files")
71 |
72 | err := fs.Parse(args)
73 | if err != nil {
74 | errorLog.Fatalf("Failed to parse command line arguments: %s", err)
75 | }
76 |
77 | if len(fs.Args()) > 1 {
78 | errorLog.Fatalf("Invalid path %s", strings.Join(fs.Args(), ","))
79 | } else if len(fs.Args()) == 1 {
80 | path = fs.Arg(0)
81 | }
82 |
83 | infoLog.Printf("Creating Certificate Authority %s with Subject: %s\n", path, *dn)
84 |
85 | if !*overwrite {
86 | checkExisting(path)
87 | }
88 |
89 | ca := filepath.Dir(path)
90 | var caCert *x509.Certificate
91 | var caKey *ecdsa.PrivateKey
92 | if ca != "." {
93 | caCert = parseCert(ca)
94 | if !caCert.IsCA {
95 | errorLog.Fatalf("Certificate %s is not a certificate authority", ca)
96 | } else if !(caCert.MaxPathLen > 0) {
97 | errorLog.Fatalf("Certificate Authority %s can't sign other certificate authorities (maxPathLength exceeded)", ca)
98 | }
99 | *maxPathLength = caCert.MaxPathLen - 1
100 | caKey = parseKey(ca)
101 | }
102 |
103 | key, derKey, err := generatePrivateKey()
104 | if err != nil {
105 | errorLog.Fatalf("Error generating private key: %s", err)
106 | }
107 |
108 | notBefore := time.Now().UTC()
109 | notAfter := notBefore.AddDate(0, 0, *validity)
110 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
111 | if err != nil {
112 | errorLog.Fatalf("Failed to generate serial number: %s", err)
113 | }
114 |
115 | template := x509.Certificate{
116 | SerialNumber: serialNumber,
117 | Subject: *parseDn(caCert, *dn),
118 | NotBefore: notBefore,
119 | NotAfter: notAfter,
120 | BasicConstraintsValid: true,
121 | IsCA: true,
122 | MaxPathLen: *maxPathLength,
123 | MaxPathLenZero: *maxPathLength == 0,
124 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
125 | }
126 |
127 | if caCert == nil {
128 | caCert = &template
129 | caKey = key
130 | }
131 |
132 | derCert, err := x509.CreateCertificate(rand.Reader, &template, caCert, &key.PublicKey, caKey)
133 | if err != nil {
134 | errorLog.Fatalf("Failed to create CA Certificate: %s", err)
135 | }
136 | saveCert(path, derCert)
137 | saveKey(path, derKey)
138 | if caCert != &template {
139 | copyFile(filepath.Join(filepath.Dir(path), "ca.pem"), filepath.Join(path, "ca.pem"), publicPerms)
140 | } else {
141 | copyFile(filepath.Join(path, path+".crt"), filepath.Join(path, "ca.pem"), publicPerms)
142 | }
143 | infoLog.Printf("Finished Creating Certificate Authority %s with Subject: %s\n", path, *dn)
144 | }
145 |
146 | func createCertificate(args []string, path string, defaultDn string, defaultSan string, defaultValidity int, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage) {
147 | fs := flag.NewFlagSet("server", flag.PanicOnError)
148 | dn := fs.String("dn", defaultDn, "certificate subject")
149 | san := fs.String("san", defaultSan, "subject alternative names")
150 | validity := fs.Int("validity", defaultValidity, "certificate validity in days")
151 | overwrite := fs.Bool("overwrite", false, "overwrite any existing files")
152 |
153 | err := fs.Parse(args)
154 | if err != nil {
155 | errorLog.Fatalf("Failed to parse command line argumanets: %s", err)
156 | }
157 |
158 | if len(fs.Args()) > 1 {
159 | errorLog.Fatalf("Invalid path %s", strings.Join(fs.Args(), ","))
160 | } else if len(fs.Args()) == 1 {
161 | path = fs.Arg(0)
162 | }
163 |
164 | infoLog.Printf("Creating Certificate %s with Subject: %s\n", path, *dn)
165 |
166 | if !*overwrite {
167 | checkExisting(path)
168 | }
169 |
170 | ca := filepath.Dir(path)
171 |
172 | caCert := parseCert(ca)
173 | if !caCert.IsCA {
174 | errorLog.Fatalf("Certificate %s is not a certificate authority", filepath.Dir(path))
175 | }
176 | caKey := parseKey(ca)
177 |
178 | key, derKey, err := generatePrivateKey()
179 | if err != nil {
180 | errorLog.Fatalf("Error generating private key: %s", err)
181 | }
182 |
183 | notBefore := time.Now().UTC().Add(-10 * time.Minute) // -10 min to mitigate clock skew
184 | notAfter := notBefore.AddDate(0, 0, *validity).Add(10 * time.Minute)
185 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
186 | if err != nil {
187 | errorLog.Fatalf("Failed to generate serial number: %s", err)
188 | }
189 |
190 | template := x509.Certificate{
191 | SerialNumber: serialNumber,
192 | Subject: *parseDn(caCert, *dn),
193 | NotBefore: notBefore,
194 | NotAfter: notAfter,
195 | IsCA: false,
196 | KeyUsage: keyUsage,
197 | ExtKeyUsage: extKeyUsage,
198 | EmailAddresses: []string{},
199 | }
200 |
201 | parseSubjectAlternativeNames(*san, &template)
202 |
203 | derCert, err := x509.CreateCertificate(rand.Reader, &template, caCert, &key.PublicKey, caKey)
204 | if err != nil {
205 | errorLog.Fatalf("Failed to create Server Certificate %s: %s", path, err)
206 | }
207 |
208 | saveCert(path, derCert)
209 | saveKey(path, derKey)
210 | copyFile(filepath.Join(filepath.Dir(path), "ca.pem"), filepath.Join(path, "ca.pem"), publicPerms)
211 | infoLog.Printf("Finished Creating Certificate %s with Subject: %s\n", path, *dn)
212 | }
213 |
214 | func parseSubjectAlternativeNames(san string, template *x509.Certificate) {
215 | infoLog.Printf("Parsing Subject Alternative Names: %s\n", san)
216 | if san != "" {
217 | template.IPAddresses = []net.IP{}
218 | template.DNSNames = []string{}
219 | for _, h := range strings.Split(san, ",") {
220 | infoLog.Printf("Parsing %s\n", h)
221 | if ip := net.ParseIP(h); ip != nil {
222 | template.IPAddresses = append(template.IPAddresses, ip)
223 | } else if email := parseEmailAddress(h); email != nil {
224 | template.EmailAddresses = append(template.EmailAddresses, email.Address)
225 | } else {
226 | template.DNSNames = append(template.DNSNames, h)
227 | }
228 | }
229 | }
230 | }
231 |
232 | // implemented as a seperate function because net.mail.ParseAddress
233 | // panics on malformed addresses
234 | func parseEmailAddress(address string) (email *mail.Address) {
235 | defer func() {
236 | if recover() != nil {
237 | email = nil
238 | }
239 | }()
240 | var err error
241 | email, err = mail.ParseAddress(address)
242 | if err == nil && email != nil {
243 | return email
244 | }
245 | return nil
246 | }
247 |
248 | func generatePrivateKey() (*ecdsa.PrivateKey, []byte, error) {
249 | key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
250 | if err != nil {
251 | return nil, nil, err
252 | }
253 | derKey, err := x509.MarshalECPrivateKey(key)
254 | if err != nil {
255 | return nil, nil, err
256 | }
257 | return key, derKey, nil
258 | }
259 |
260 | func checkExisting(path string) {
261 | fullPath := filepath.Join(path, filepath.Base(path))
262 | const errMsg = "Skipping creation of %s because file %s already exists.\nUse the \"-overwrite\" option to overwrite the existing file."
263 | if _, err := os.Stat(fullPath + ".crt"); err == nil {
264 | errorLog.Fatalf(errMsg, path, "./"+fullPath+".crt")
265 | }
266 | if _, err := os.Stat(fullPath + ".crt"); err == nil {
267 | errorLog.Fatalf(errMsg, path, "./"+fullPath+".key")
268 | }
269 | if _, err := os.Stat(filepath.Join(path, "ca.pem")); err == nil {
270 | errorLog.Fatalf("Skipping creation of %s because file %s already exists.\nUse the \"-overwrite\" option to overwrite the existing file.", path, filepath.Join(path, "ca.pem"))
271 | }
272 | }
273 |
274 | func createDirectory(directory string) {
275 | if _, err := os.Stat(directory); os.IsNotExist(err) {
276 | var publicPerms os.FileMode = 0755
277 | if err := os.MkdirAll(directory, publicPerms); err != nil {
278 | errorLog.Fatalf("Error creating directory ./%s: %s", directory, err.Error())
279 | }
280 | }
281 | }
282 |
283 | func saveCert(directory string, derCert []byte) {
284 | createDirectory(directory)
285 |
286 | fileName := filepath.Join(directory, filepath.Base(directory)+".crt")
287 |
288 | infoLog.Printf("Saving %s\n", fileName)
289 |
290 | certFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, publicPerms)
291 | if err != nil {
292 | errorLog.Fatalf("Failed to open %s for writing: %s", fileName, err)
293 | }
294 | defer func() {
295 | if err := certFile.Close(); err != nil {
296 | errorLog.Fatalf("Failed to save %s: %s", fileName, err)
297 | }
298 | }()
299 | if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: derCert}); err != nil {
300 | errorLog.Fatalf("Failed to marshall %s: %s", fileName, err)
301 | }
302 | if filepath.Dir(directory) != "." {
303 | caFile, err := os.Open(filepath.Join(filepath.Dir(directory), filepath.Base(filepath.Dir(directory))) + ".crt")
304 | if err != nil {
305 | errorLog.Fatalf("Failed to open ca certificate: %s", err)
306 | }
307 | defer func() {
308 | if err = caFile.Close(); err != nil {
309 | errorLog.Fatalf("Failed to close %s: %s", filepath.Join(filepath.Dir(directory), filepath.Base(filepath.Dir(directory)))+".crt", err)
310 | }
311 | }()
312 | _, err = io.Copy(certFile, caFile)
313 | if err != nil {
314 | errorLog.Fatalf("Failed to concat ca certificates: %s", err)
315 | }
316 | err = certFile.Sync()
317 | if err != nil {
318 | errorLog.Fatalf("Failed to sync certificate file: %s", err)
319 | }
320 | }
321 | }
322 |
323 | func saveKey(directory string, derKey []byte) {
324 |
325 | fileName := filepath.Join(directory, filepath.Base(directory)+".key")
326 |
327 | infoLog.Printf("Saving %s\n", fileName)
328 |
329 | keyFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, privatePerms)
330 | if err != nil {
331 | errorLog.Fatalf("Failed to open %s for writing: %s", fileName, err)
332 | }
333 | defer func() {
334 | if err := keyFile.Close(); err != nil {
335 | errorLog.Fatalf("Failed to close %s: %s", fileName, err)
336 | }
337 | }()
338 | if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: derKey}); err != nil {
339 | errorLog.Fatalf("Failed to marshall %s: %s", fileName, err)
340 | }
341 | }
342 |
343 | func parseCert(path string) *x509.Certificate {
344 | der, err := ioutil.ReadFile(filepath.Join(path, filepath.Base(path)+".crt"))
345 | if err != nil {
346 | errorLog.Fatalf("Failed to read certificate file %s: %s", filepath.Join(path, filepath.Base(path)+".crt"), err)
347 | }
348 | block, _ := pem.Decode(der)
349 | if block == nil || block.Type != "CERTIFICATE" {
350 | errorLog.Fatalf("Failed to decode certificate %s: %s", filepath.Join(path, filepath.Base(path)+".crt"), err)
351 | }
352 | crt, err := x509.ParseCertificate(block.Bytes)
353 | if err != nil {
354 | errorLog.Fatalf("Failed to parse certificate %s: %s", filepath.Join(path, filepath.Base(path)+".crt"), err)
355 | }
356 | return crt
357 | }
358 |
359 | func parseKey(path string) *ecdsa.PrivateKey {
360 | der, err := ioutil.ReadFile(filepath.Join(path, filepath.Base(path)+".key"))
361 | if err != nil {
362 | errorLog.Fatalf("Failed to read private key file %s: %s", filepath.Join(path, filepath.Base(path)+".key"), err)
363 | }
364 | block, _ := pem.Decode(der)
365 | if block == nil || block.Type != "EC PRIVATE KEY" {
366 | errorLog.Fatalf("Failed to decode private key for %s: %s", filepath.Join(path, filepath.Base(path)+".key"), err)
367 | }
368 | key, err := x509.ParseECPrivateKey(block.Bytes)
369 | if err != nil {
370 | errorLog.Fatalf("Failed to parse private key for %s: %s", filepath.Join(path, filepath.Base(path)+".key"), err)
371 | }
372 | return key
373 | }
374 |
375 | func parseDn(ca *x509.Certificate, dn string) *pkix.Name {
376 | infoLog.Printf("Parsing distinguished name: %s\n", dn)
377 | var caName pkix.Name
378 | if ca != nil {
379 | caName = ca.Subject
380 | } else {
381 | caName = pkix.Name{}
382 | }
383 | newName := &pkix.Name{}
384 | for _, element := range strings.Split(strings.Trim(dn, "/"), "/") {
385 | value := strings.Split(element, "=")
386 | if len(value) != 2 {
387 | errorLog.Fatalf("Failed to parse distinguised name: malformed element %s in dn", element)
388 | }
389 | switch strings.ToUpper(value[0]) {
390 | case "CN": // commonName
391 | newName.CommonName = value[1]
392 | case "C": // countryName
393 | if value[1] == "" {
394 | caName.Country = []string{}
395 | } else {
396 | newName.Country = append(newName.Country, value[1])
397 | }
398 | case "L": // localityName
399 | if value[1] == "" {
400 | caName.Locality = []string{}
401 | } else {
402 | newName.Locality = append(newName.Locality, value[1])
403 | }
404 | case "ST": // stateOrProvinceName
405 | if value[1] == "" {
406 | caName.Province = []string{}
407 | } else {
408 | newName.Province = append(newName.Province, value[1])
409 | }
410 | case "O": // organizationName
411 | if value[1] == "" {
412 | caName.Organization = []string{}
413 | } else {
414 | newName.Organization = append(newName.Organization, value[1])
415 | }
416 | case "OU": // organizationalUnitName
417 | if value[1] == "" {
418 | caName.OrganizationalUnit = []string{}
419 | } else {
420 | newName.OrganizationalUnit = append(newName.OrganizationalUnit, value[1])
421 | }
422 | default:
423 | errorLog.Fatalf("Failed to parse distinguised name: unknown element %s", element)
424 | }
425 | }
426 | if ca != nil {
427 | newName.Country = append(caName.Country, newName.Country...)
428 | newName.Locality = append(caName.Locality, newName.Locality...)
429 | newName.Province = append(caName.Province, newName.Province...)
430 | newName.Organization = append(caName.Organization, newName.Organization...)
431 | newName.OrganizationalUnit = append(caName.OrganizationalUnit, newName.OrganizationalUnit...)
432 | }
433 | return newName
434 | }
435 |
436 | func exportCertificate(args []string) {
437 | fs := flag.NewFlagSet("export", flag.PanicOnError)
438 | crt := fs.Bool("crt", true, "include the certificate in pem format")
439 | key := fs.Bool("key", true, "include the private key in pem format")
440 | ca := fs.Bool("ca", true, "include the ca bundle in pem format")
441 | p12 := fs.Bool("p12", false, "include certificate and key together in pkcs12 format")
442 | password := fs.String("password", "", "password for pkcs12 format")
443 | openvpn := fs.Bool("openvpn", false, "include snippet that can be concatenated to the end of openvpn config files")
444 |
445 | err := fs.Parse(args)
446 | if err != nil {
447 | errorLog.Fatalf("Failed to parse command line arguments: %s", err)
448 | }
449 |
450 | if len(fs.Args()) != 1 {
451 | errorLog.Fatalf("Invalid path %s", strings.Join(fs.Args(), ","))
452 | }
453 | path := fs.Arg(0)
454 | name := filepath.Base(path)
455 | infoLog.Printf("Exporting Certificate %s", path)
456 |
457 | gz := gzip.NewWriter(os.Stdout)
458 | defer func() {
459 | if err = gz.Close(); err != nil {
460 | errorLog.Fatalf("Failed to close gzip writer: %s", err)
461 | }
462 | }()
463 |
464 | tw := tar.NewWriter(gz)
465 | defer func() {
466 | if err = tw.Close(); err != nil {
467 | errorLog.Fatalf("Failed to close tar file: %s", err)
468 | }
469 | }()
470 | if *p12 {
471 | if *password == "" {
472 | errorLog.Fatalf("A password is required to export to pkcs12 format")
473 | }
474 | infoLog.Print("Running openssl to create p12 file")
475 | cmd := exec.Command("openssl", "pkcs12", "-export", "-in", filepath.Join(path, name+".crt"), "-inkey", filepath.Join(path, name+".key"), "-passout", "stdin")
476 | stdin, err := cmd.StdinPipe()
477 | if err != nil {
478 | errorLog.Fatalf("Failed to open stdin pipe to openssl: %s", err)
479 | }
480 | go func() {
481 | defer func() {
482 | if err = stdin.Close(); err != nil {
483 | errorLog.Fatalf("Failed to close stdin pipe to openssl: %s", err)
484 | }
485 | }()
486 | if _, err = io.WriteString(stdin, *password); err != nil {
487 | errorLog.Fatalf("Failed to transfer password to openssl: %s", err)
488 | }
489 | }()
490 | out, err := cmd.Output()
491 | if err != nil {
492 | errorLog.Fatalf("Error running openssl: %s", err)
493 | }
494 | header := &tar.Header{Name: name + ".p12", Mode: 0600, ModTime: time.Now().UTC(), Size: int64(len(out))}
495 | if err = tw.WriteHeader(header); err != nil {
496 | errorLog.Fatalf("Failed to write tar header: %s", err)
497 | }
498 | if _, err = tw.Write(out); err != nil {
499 | errorLog.Fatalf("Failed to write tar file: %s", err)
500 | }
501 | infoLog.Print("Finished running openssl")
502 | }
503 | if *crt {
504 | tarAppendFile(tw, filepath.Join(path, name+".crt"), name+".crt", "cert.pem", 0644)
505 | }
506 | if *key {
507 | tarAppendFile(tw, filepath.Join(path, name+".key"), name+".key", "key.pem", 0600)
508 | }
509 | if *ca {
510 | tarAppendFile(tw, filepath.Join(path, "ca.pem"), "ca.pem", "", 0644)
511 | }
512 | if *openvpn {
513 | type config struct {
514 | Ca, Cert, Key string
515 | }
516 | text := "# Append this snippet to the end of the OpenVPN config file\n\n{{.Ca}}\n\n{{.Cert}}\n\n{{.Key}}\n"
517 | tmpl, err := template.New("ovpn").Parse(text)
518 | if err != nil {
519 | errorLog.Fatalf("Error parsing ovpn config template: %s", err)
520 | }
521 | buf := new(bytes.Buffer)
522 | if err = tmpl.Execute(buf,
523 | config{Ca: readFile(filepath.Join(path, "ca.pem")),
524 | Cert: readFile(filepath.Join(path, name+".crt")),
525 | Key: readFile(filepath.Join(path, name+".key"))}); err != nil {
526 | errorLog.Fatalf("Error creating ovpn config: %s", err)
527 | }
528 | header := &tar.Header{Name: name + ".ovpn", Mode: 0600, ModTime: time.Now().UTC(), Size: int64(buf.Len())}
529 | if err = tw.WriteHeader(header); err != nil {
530 | errorLog.Fatalf("Failed to write tar header: %s", err)
531 | }
532 | if _, err = tw.Write(buf.Bytes()); err != nil {
533 | errorLog.Fatalf("Failed to write tar file: %s", err)
534 | }
535 | }
536 | infoLog.Printf("Finished Exporting Certificate %s", path)
537 | }
538 |
539 | func tarAppendFile(tw *tar.Writer, path string, tarPath string, altTarPath string, mode int64) {
540 | info, err := os.Stat(path)
541 | if err != nil {
542 | errorLog.Fatalf("Failed to read file metadata: %s", path)
543 | }
544 | file, err := os.Open(path)
545 | if err != nil {
546 | errorLog.Fatalf("Failed to open file: %s", path)
547 | }
548 | defer func() {
549 | if err = file.Close(); err != nil {
550 | errorLog.Fatalf("Failed to close %s: %s", path, err)
551 | }
552 | }()
553 | if err := tw.WriteHeader(&tar.Header{Name: tarPath, Mode: mode, ModTime: info.ModTime(), Size: info.Size()}); err != nil {
554 | errorLog.Fatalf("Failed to write tar header: %s", path)
555 | }
556 | if _, err := io.Copy(tw, file); err != nil {
557 | errorLog.Fatalf("Failed to write tar file: %s", path)
558 | }
559 | if altTarPath != "" {
560 | if err := tw.WriteHeader(&tar.Header{Name: altTarPath, Mode: mode, ModTime: info.ModTime(), Linkname: tarPath, Typeflag: tar.TypeLink}); err != nil {
561 | errorLog.Fatalf("Failed to create hard links in tar file: %s", path)
562 | }
563 | }
564 | }
565 |
566 | func copyFile(source string, dest string, perms os.FileMode) {
567 | sourceFile, err := os.Open(source)
568 | if err != nil {
569 | errorLog.Fatalf("Failed to open %s for reading: %s", source, err)
570 | }
571 | defer func() {
572 | if err = sourceFile.Close(); err != nil {
573 | errorLog.Fatalf("Failed to close %s: %s", source, err)
574 | }
575 | }()
576 | destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, perms)
577 | if err != nil {
578 | errorLog.Fatalf("Failed to open %s for writing: %s", dest, err)
579 | }
580 | defer func() {
581 | if err = destFile.Close(); err != nil {
582 | errorLog.Fatalf("Failed to close %s: %s", dest, err)
583 | }
584 | }()
585 | if _, err = io.Copy(destFile, sourceFile); err != nil {
586 | errorLog.Fatalf("Failed to copy %s: %s", source, err)
587 | }
588 | }
589 |
590 | func readFile(path string) string {
591 | data, err := ioutil.ReadFile(path)
592 | if err != nil {
593 | errorLog.Fatalf("Failed to read file %s: %s", path, err)
594 | }
595 | return string(data)
596 | }
597 |
--------------------------------------------------------------------------------