diff --git a/dumper.go b/dumper.go new file mode 100644 index 0000000..2e5cebe --- /dev/null +++ b/dumper.go @@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + "encoding/pem" + "io/ioutil" + "os" + "path/filepath" + + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/registration" +) + +// StoredData represents the data managed by the Store +type StoredData struct { + Account *Account + Certificates []*Certificate + HTTPChallenges map[string]map[string][]byte + TLSChallenges map[string]*Certificate +} + +// Certificate is a struct which contains all data needed from an ACME certificate +type Certificate struct { + Domain Domain + Certificate []byte + Key []byte +} + +// Domain holds a domain name with SANs +type Domain struct { + Main string + SANs []string +} + +// Account is used to store lets encrypt registration info +type Account struct { + Email string + Registration *registration.Resource + PrivateKey []byte + KeyType certcrypto.KeyType +} + +func dump(acmeFile, dumpPath string) error { + f, err := os.Open(acmeFile) + if err != nil { + return err + } + + data := StoredData{} + if err = json.NewDecoder(f).Decode(&data); err != nil { + return err + } + + if err = os.RemoveAll(dumpPath); err != nil { + return err + } + + err = os.MkdirAll(filepath.Join(dumpPath, "certs"), 0755) + if err != nil { + return err + } + + err = os.MkdirAll(filepath.Join(dumpPath, "private"), 0755) + if err != nil { + return err + } + + privateKeyPem := extractPEMPrivateKey(data.Account) + err = ioutil.WriteFile(filepath.Join(dumpPath, "private", "letsencrypt.key"), privateKeyPem, 0666) + if err != nil { + return err + } + + for _, cert := range data.Certificates { + err = ioutil.WriteFile(filepath.Join(dumpPath, "private", cert.Domain.Main+".key"), cert.Key, 0666) + if err != nil { + return err + } + + err = ioutil.WriteFile(filepath.Join(dumpPath, "certs", cert.Domain.Main+".crt"), cert.Certificate, 0666) + if err != nil { + return err + } + } + + return nil +} + +func extractPEMPrivateKey(account *Account) []byte { + var block *pem.Block + switch account.KeyType { + case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192: + block = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: account.PrivateKey, + } + case certcrypto.EC256, certcrypto.EC384: + block = &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: account.PrivateKey, + } + default: + panic("unsupported key type") + } + + return pem.EncodeToMemory(block) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b85c59f --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/ldez/treafik-cert-dumper + +require ( + github.com/cenkalti/backoff v2.1.1+incompatible // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 // indirect + github.com/xenolf/lego v2.2.0+incompatible + golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f // indirect + gopkg.in/square/go-jose.v2 v2.2.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6707df8 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/xenolf/lego v2.2.0+incompatible h1:r4UAcpgPmX3j0aThoVrRM1FFLcvyy08UyGbIwFU4zoQ= +github.com/xenolf/lego v2.2.0+incompatible/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= +golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f h1:ETU2VEl7TnT5bl7IvuKEzTDpplg5wzGYsOCAPhdoEIg= +golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2cbcb04 --- /dev/null +++ b/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/cobra" +) + +func main() { + var rootCmd = &cobra.Command{ + Use: "traefik-certs-dumper", + Short: "Dump Let's Encrypt certificates from Traefik", + Long: `Dump the content of the "acme.json" file from Traefik to certificates.`, + Version: version, + } + + var dumpCmd = &cobra.Command{ + Use: "dump", + Short: "Dump Let's Encrypt certificates from Traefik", + Long: `Dump the content of the "acme.json" file from Traefik to certificates.`, + Run: func(cmd *cobra.Command, _ []string) { + acmeFile := cmd.Flag("source").Value.String() + dumpPath := cmd.Flag("dest").Value.String() + + err := dump(acmeFile, dumpPath) + if err != nil { + log.Fatal(err) + } + }, + } + + dumpCmd.Flags().String("source", "./acme.json", "Path to 'acme.json' file.") + dumpCmd.Flags().String("dest", "./dump", "Path to store the dump content.") + rootCmd.AddCommand(dumpCmd) + + var versionCmd = &cobra.Command{ + Use: "version", + Short: "Display version", + Run: func(_ *cobra.Command, _ []string) { + displayVersion(rootCmd.Name()) + }, + } + + rootCmd.AddCommand(versionCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..a101709 --- /dev/null +++ b/version.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "runtime" +) + +var ( + version = "dev" + commit = "I don't remember exactly" + date = "I don't remember exactly" +) + +func displayVersion(name string) { + fmt.Printf(name+`: + version : %s + commit : %s + build date : %s + go version : %s + go compiler : %s + platform : %s/%s +`, version, commit, date, runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH) +}