diff --git a/Dockerfile b/Dockerfile index 30d3d1d..3ec65eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,6 @@ WORKDIR /root/ COPY --from=builder /etc/ssl/certs /etc/ssl/certs COPY --from=builder /go/src/github.com/project0/certjunkie/certjunkie . -ENTRYPOINT ["./certjunkie"] \ No newline at end of file +ENTRYPOINT ["./certjunkie"] + +CMD [ "server" ] \ No newline at end of file diff --git a/api/client.go b/api/client.go new file mode 100644 index 0000000..71cfa16 --- /dev/null +++ b/api/client.go @@ -0,0 +1,77 @@ +package api + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/project0/certjunkie/certstore" +) + +// Client talks with the API +type Client struct { + Address string +} + +// Get retrieves the cert, private key and ca bundle +func (c *Client) Get(domain string, san []string, onlyCN bool, valid int) (cert *certstore.CertificateResource, err error) { + + var ( + resp *http.Response + u *url.URL + ) + client := http.DefaultClient + + u, err = url.Parse(c.Address + "/cert/" + domain) + if err != nil { + return + } + + // Add queries + q := u.Query() + if onlyCN { + q.Set("onlycn", "1") + } + if valid != 0 { + q.Set("valid", strconv.Itoa(valid)) + } + if len(san) > 0 { + q.Set("san", strings.Join(san, ",")) + } + u.RawQuery = q.Encode() + + resp, err = client.Get(u.String()) + if err != nil { + return + } + + err = json.NewDecoder(resp.Body).Decode(cert) + return +} + +// WriteCert writes the cert to file +func (c *Client) WriteCert(cert *certstore.CertificateResource, filepath string) (err error) { + return c.writeFile(cert.Certificate, filepath) +} + +// WriteBundle writes the cert + ca to file +func (c *Client) WriteBundle(cert *certstore.CertificateResource, filepath string) (err error) { + return c.writeFile(append(cert.Certificate, cert.IssuerCertificate...), filepath) +} + +// WriteKey writes the privte key to file +func (c *Client) WriteKey(cert *certstore.CertificateResource, filepath string) (err error) { + return c.writeFile(cert.PrivateKey, filepath) +} + +// WriteCA writes the ca chain to file +func (c *Client) WriteCA(cert *certstore.CertificateResource, filepath string) (err error) { + return c.writeFile(cert.IssuerCertificate, filepath) +} + +func (c *Client) writeFile(data []byte, filepath string) error { + return ioutil.WriteFile(filepath, data, 0644) +} diff --git a/main.go b/main.go index 8a4c199..5357569 100644 --- a/main.go +++ b/main.go @@ -1,81 +1,244 @@ package main import ( + "fmt" "log" "os" "os/signal" + "strings" "syscall" "github.com/docker/libkv" "github.com/docker/libkv/store" - "github.com/spf13/pflag" - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/providers/dns" - "github.com/project0/certjunkie/api" "github.com/project0/certjunkie/certstore" "github.com/project0/certjunkie/certstore/libkv/local" "github.com/project0/certjunkie/provider" + "github.com/urfave/cli" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns" ) const ACME_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory" const ACME = "https://acme-v02.api.letsencrypt.org/directory" +const envPrefix = "CJ" + var certStore *certstore.CertStore +func flagSetHelperEnvKey(name string) string { + envKey := strings.ToUpper(name) + envKey = strings.Replace(envKey, "-", "_", -1) + return envPrefix + "_" + envKey +} + func main() { - AcmeServer := pflag.String("server", ACME, "ACME Directory Resource URI") - Email := pflag.String("email", "", "Registration email for the ACME server") - ApiListen := pflag.String("listen", ":80", "Bind on this port to run the API server on") - ChallengeProvider := pflag.String("provider", "dnscname", "DNS challenge provider name") - DnsListen := pflag.String("dns.listen", ":53", "Bind on this port to run the DNS server on (tcp and udp)") - DnsDomain := pflag.String("dns.domain", "ns.local", "The NS domain name of this server") - DnsZone := pflag.String("dns.zone", "acme.local", "The zone we are using to provide the txt records for challenge") - StorageDriver := pflag.String("storage", "local", "Storage driver to use, currently only local is supported") - StorageLocalPath := pflag.String("storage.local", os.Getenv("HOME")+"/.certjunkie", "Path to store the certs and account data for local storage driver") - pflag.Parse() - - if *Email == "" { - log.Fatal("Email is not set") - } - if *DnsDomain == "" { - log.Fatal("DNS Domain is not set") - } - if *DnsZone == "" { - log.Fatal("Dns Zone is not set") - } - if *DnsZone == "" { - log.Fatal("Dns Zone is not set") - } - local.Register() - storage, err := libkv.NewStore(store.Backend(*StorageDriver), []string{}, &store.Config{ - Bucket: *StorageLocalPath, - }) - if err != nil { - log.Fatal(err) - } + app := cli.NewApp() + app.HideVersion = true - var dnsprovider acme.ChallengeProvider - if *ChallengeProvider == "dnscname" { - // use built in dns server for cname redirect - dnsprovider = provider.NewDNSCnameChallengeProvider(*DnsZone, *DnsDomain, *DnsListen) - } else { - // one of the shipped lego providers - dnsprovider, err = dns.NewDNSChallengeProviderByName(*ChallengeProvider) - if err != nil { - log.Fatal(err) - } - } + app.Commands = []cli.Command{ + { + Name: "server", + Description: "run DNS and API server", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "server", + Value: ACME, + Usage: "ACME Directory Resource URI", + EnvVar: flagSetHelperEnvKey("SERVER"), + }, + cli.StringFlag{ + Name: "email", + Usage: "Registration email for the ACME server", + EnvVar: flagSetHelperEnvKey("EMAIL"), + }, + cli.StringFlag{ + Name: "listen", + Value: ":80", + Usage: "Bind listener address for http (api) server", + EnvVar: flagSetHelperEnvKey("LISTEN"), + }, + cli.StringFlag{ + Name: "provider", + Value: provider.Name, + Usage: "DNS challenge provider name", + EnvVar: flagSetHelperEnvKey("PROVIDER"), + }, + cli.StringFlag{ + Name: "dns.listen", + Value: ":53", + Usage: "Bind on this port to run the DNS server on (tcp and udp)", + EnvVar: flagSetHelperEnvKey("DNS_LISTEN"), + }, + cli.StringFlag{ + Name: "dns.domain", + Value: "ns.local", + Usage: "The NS domain name of this server", + EnvVar: flagSetHelperEnvKey("DNS_DOMAIN"), + }, + cli.StringFlag{ + Name: "dns.zone", + Value: "acme.local", + Usage: "The zone we are using to provide the txt records for challenge", + EnvVar: flagSetHelperEnvKey("DNS_ZONE"), + }, + cli.StringFlag{ + Name: "storage", + Value: "local", + Usage: "Storage driver to use, currently only local is supported", + EnvVar: flagSetHelperEnvKey("STORAGE"), + }, + cli.StringFlag{ + Name: "storage.path", + Value: os.Getenv("HOME") + "/.certjunkie", + Usage: "Path to store the certs and account data for local storage driver", + EnvVar: flagSetHelperEnvKey("STORAGE_PATH"), + }, + }, + Action: func(c *cli.Context) error { + email := c.String("email") + challengeProvider := c.String("provider") + if email == "" { + return fmt.Errorf("Email is not set") + } + + local.Register() + storage, err := libkv.NewStore(store.Backend(c.String("storage")), []string{}, &store.Config{ + Bucket: c.String("storage.path"), + }) + if err != nil { + log.Fatal(err) + } + + var dnsprovider acme.ChallengeProvider + if challengeProvider == provider.Name { + // use built in dns server for cname redirect + + dnsprovider = provider.NewDNSCnameChallengeProvider(c.String("dns.zone"), c.String("dns.domain"), c.String("dns.listen")) + } else { + // one of the shipped lego providers + dnsprovider, err = dns.NewDNSChallengeProviderByName(challengeProvider) + if err != nil { + log.Fatal(err) + } + } + + certStore, err = certstore.NewCertStore(c.String("server"), email, &dnsprovider, storage) + if err != nil { + log.Fatal(err) + } - certStore, err = certstore.NewCertStore(*AcmeServer, *Email, &dnsprovider, storage) - if err != nil { - log.Fatal(err) + api.NewApiServer(c.String("listen"), certStore) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + storage.Close() + + return nil + }, + }, + { + Name: "client", + Description: "client to retrieve cert bundle from an certjunkie api", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "address", + Value: "http://localhost:80", + Usage: "CertJunkie api address", + EnvVar: flagSetHelperEnvKey("CLIENT_ADDRESS"), + }, + cli.StringFlag{ + Name: "domain", + Usage: "Domain (common name) to obtain cert for, wildcard is allowed to use here", + EnvVar: flagSetHelperEnvKey("CLIENT_DOMAIN"), + }, + cli.BoolFlag{ + Name: "onlycn", + Usage: "Retrieve only certs where the common name is matching the domain", + EnvVar: flagSetHelperEnvKey("CLIENT_ONLYCN"), + }, + cli.StringSliceFlag{ + Name: "san", + Usage: "Additonal subject alternative names (domains) the cert must have", + EnvVar: flagSetHelperEnvKey("CLIENT_SAN"), + }, + cli.IntFlag{ + Name: "valid", + Usage: " How long needs the cert to be valid in days before requesting a new on", + EnvVar: flagSetHelperEnvKey("CLIENT_VALID"), + }, + cli.StringFlag{ + Name: "file.cert", + Usage: "Write certificate to file", + EnvVar: flagSetHelperEnvKey("CLIENT_FILE_CERT"), + }, + cli.StringFlag{ + Name: "file.ca", + Usage: "Write ca issuer to file", + EnvVar: flagSetHelperEnvKey("CLIENT_FILE_CA"), + }, + cli.StringFlag{ + Name: "file.key", + Usage: "Write private key to file", + EnvVar: flagSetHelperEnvKey("CLIENT_FILE_KEY"), + }, + cli.StringFlag{ + Name: "file.bundle", + Usage: "Write bundle (cert+ca) to file", + EnvVar: flagSetHelperEnvKey("CLIENT_FILE_BUNDLE"), + }, + }, + Action: func(c *cli.Context) error { + domain := c.String("domain") + if domain == "" { + return fmt.Errorf("Domain is not set") + } + + client := &api.Client{ + Address: c.String("address"), + } + + cert, err := client.Get(domain, c.StringSlice("san"), c.Bool("onlycn"), c.Int("valid")) + if err != nil { + return err + } + + // write result to files + fileCert := c.String("file.cert") + fileCA := c.String("file.ca") + fileKey := c.String("file.key") + fileBundle := c.String("file.bundle") + if fileCert != "" { + err := client.WriteCert(cert, fileCert) + if err != nil { + return err + } + } + if fileCA != "" { + err := client.WriteCA(cert, fileCA) + if err != nil { + return err + } + } + if fileKey != "" { + err := client.WriteCA(cert, fileKey) + if err != nil { + return err + } + } + if fileBundle != "" { + err := client.WriteCA(cert, fileBundle) + if err != nil { + return err + } + } + + return nil + }, + }, } - api.NewApiServer(*ApiListen, certStore) - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs - storage.Close() + app.RunAndExitOnError() + } diff --git a/provider/dnscname.go b/provider/dnscname.go index 95d9b03..a159543 100644 --- a/provider/dnscname.go +++ b/provider/dnscname.go @@ -11,6 +11,8 @@ import ( "github.com/xenolf/lego/acme" ) +const Name = "dnscname" + var ChallengeLocker = make(map[string]*sync.Mutex) var ChallengeRecord = make(map[string]*dns.TXT)