├── 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 | --------------------------------------------------------------------------------