commit ea47f1137a1ad9b2bf375f4e276137147cdfc689 Author: xenolf Date: Mon Jun 8 02:36:07 2015 +0200 Base implementation with registration support diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..31e1e36d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +lego.exe diff --git a/account.go b/account.go new file mode 100644 index 00000000..aa1f7b83 --- /dev/null +++ b/account.go @@ -0,0 +1,96 @@ +package main + +import ( + "crypto/rsa" + "encoding/json" + "io/ioutil" + "os" + "path" + + "github.com/xenolf/lego/acme" +) + +// Account represents a users local saved credentials +type Account struct { + Email string `json:"email"` + key *rsa.PrivateKey + Registration *acme.RegistrationResource `json:"registration"` + + conf *Configuration +} + +// NewAccount creates a new account for an email address +func NewAccount(email string, conf *Configuration) *Account { + accKeysPath := conf.AccountKeysPath(email) + // TODO: move to function in configuration? + accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key" + if err := checkFolder(accKeysPath); err != nil { + logger().Fatalf("Could not check/create directory for account %s: %v", email, err) + } + + var privKey *rsa.PrivateKey + if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { + logger().Printf("No key found for account %s. Generating a %v bit key.", email, conf.RsaBits()) + privKey, err = generateRsaKey(conf.RsaBits(), accKeyPath) + if err != nil { + logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err) + } + logger().Printf("Saved key to %s", accKeyPath) + } else { + privKey, err = loadRsaKey(accKeyPath) + if err != nil { + logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) + } + } + + accountFile := path.Join(conf.AccountPath(email), "account.json") + if _, err := os.Stat(accountFile); os.IsNotExist(err) { + return &Account{Email: email, key: privKey, conf: conf} + } + + fileBytes, err := ioutil.ReadFile(accountFile) + if err != nil { + logger().Fatalf("Could not load file for account %s -> %v", email, err) + } + + var acc Account + err = json.Unmarshal(fileBytes, &acc) + if err != nil { + logger().Fatalf("Could not parse file for account %s -> %v", email, err) + } + + acc.key = privKey + acc.conf = conf + + return &acc +} + +/** Implementation of the acme.User interface **/ + +// GetEmail returns the email address for the account +func (a *Account) GetEmail() string { + return a.Email +} + +// GetPrivateKey returns the private RSA account key. +func (a *Account) GetPrivateKey() *rsa.PrivateKey { + return a.key +} + +// GetRegistration returns the server registration +func (a *Account) GetRegistration() *acme.RegistrationResource { + return a.Registration +} + +/** End **/ + +// Save the account to disk +func (a *Account) Save() error { + jsonBytes, err := json.Marshal(a) + if err != nil { + return err + } + + return ioutil.WriteFile(path.Join(a.conf.AccountPath(a.Email), "account.json"), jsonBytes, 0700) + +} diff --git a/acme/client.go b/acme/client.go new file mode 100644 index 00000000..eb36700d --- /dev/null +++ b/acme/client.go @@ -0,0 +1,144 @@ +package acme + +import ( + "bytes" + "crypto/rsa" + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "os" + "regexp" + "strings" + + "github.com/square/go-jose" +) + +// Logger is used to log errors; if nil, the default log.Logger is used. +var Logger *log.Logger + +// logger is an helper function to retrieve the available logger +func logger() *log.Logger { + if Logger == nil { + Logger = log.New(os.Stderr, "", log.LstdFlags) + } + return Logger +} + +// User interface is to be implemented by users of this library. +// It is used by the client type to get user specific information. +type User interface { + GetEmail() string + GetRegistration() *RegistrationResource + GetPrivateKey() *rsa.PrivateKey +} + +// Client is the user-friendy way to ACME +type Client struct { + regURL string + user User +} + +// NewClient creates a new client for the set user. +func NewClient(caURL string, usr User) *Client { + if err := usr.GetPrivateKey().Validate(); err != nil { + logger().Fatalf("Could not validate the private account key of %s -> %v", usr.GetEmail(), err) + } + + return &Client{regURL: caURL, user: usr} +} + +// Posts a JWS signed message to the specified URL +func (c *Client) jwsPost(url string, content []byte) (*http.Response, error) { + signer, err := jose.NewSigner(jose.RS256, c.user.GetPrivateKey()) + if err != nil { + return nil, err + } + + signed, err := signer.Sign(content) + if err != nil { + return nil, err + } + signedContent := signed.FullSerialize() + + resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(signedContent))) + if err != nil { + return nil, err + } + + return resp, err +} + +// Register the current account to the ACME server. +func (c *Client) Register() (*RegistrationResource, error) { + logger().Print("Registering account ... ") + jsonBytes, err := json.Marshal(registrationMessage{Contact: []string{"mailto:" + c.user.GetEmail()}}) + if err != nil { + return nil, err + } + + resp, err := c.jwsPost(c.regURL, jsonBytes) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusConflict { + // REVIEW: should this return an error? + return nil, errors.New("This account is already registered with this CA.") + } + + var serverReg Registration + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&serverReg) + if err != nil { + return nil, err + } + + reg := &RegistrationResource{Body: serverReg} + + links := parseLinks(resp.Header["Link"]) + reg.URI = resp.Header.Get("Location") + if links["terms-of-service"] != "" { + reg.TosURL = links["terms-of-service"] + } + + if links["next"] != "" { + reg.NewAuthzURL = links["next"] + } else { + return nil, errors.New("The server did not return enough information to proceed...") + } + + return reg, nil +} + +func logResponseHeaders(resp *http.Response) { + logger().Println(resp.Status) + for k, v := range resp.Header { + logger().Printf("-- %s: %s", k, v) + } +} + +func logResponseBody(resp *http.Response) { + body, _ := ioutil.ReadAll(resp.Body) + logger().Printf("Returned json data: \n%s", body) +} + +func parseLinks(links []string) map[string]string { + aBrkt := regexp.MustCompile("[<>]") + slver := regexp.MustCompile("(.+) *= *\"(.+)\"") + linkMap := make(map[string]string) + + for _, link := range links { + + link = aBrkt.ReplaceAllString(link, "") + parts := strings.Split(link, ";") + + matches := slver.FindStringSubmatch(parts[1]) + if len(matches) > 0 { + linkMap[matches[2]] = parts[0] + } + } + + return linkMap +} diff --git a/acme/messages.go b/acme/messages.go new file mode 100644 index 00000000..5f53b400 --- /dev/null +++ b/acme/messages.go @@ -0,0 +1,27 @@ +package acme + +type registrationMessage struct { + Contact []string `json:"contact"` +} + +// Registration is returned by the ACME server after the registration +// The client implementation should save this registration somewhere. +type Registration struct { + ID int `json:"id"` + Key struct { + Kty string `json:"kty"` + N string `json:"n"` + E string `json:"e"` + } `json:"key"` + Recoverytoken string `json:"recoveryToken"` + Contact []string `json:"contact"` +} + +// RegistrationResource represents all important informations about a registration +// of which the client needs to keep track itself. +type RegistrationResource struct { + Body Registration + URI string + NewAuthzURL string + TosURL string +} diff --git a/cli.go b/cli.go new file mode 100644 index 00000000..b45a9309 --- /dev/null +++ b/cli.go @@ -0,0 +1,192 @@ +package main + +import ( + "log" + "os" + + "github.com/codegangsta/cli" + "github.com/xenolf/lego/acme" +) + +// Logger is used to log errors; if nil, the default log.Logger is used. +var Logger *log.Logger + +// logger is an helper function to retrieve the available logger +func logger() *log.Logger { + if Logger == nil { + Logger = log.New(os.Stderr, "", log.LstdFlags) + } + return Logger +} + +func main() { + + app := cli.NewApp() + app.Name = "lego" + app.Usage = "Let's encrypt client to go!" + app.Version = "0.0.1" + + app.Commands = []cli.Command{ + { + Name: "run", + Usage: "Create and install a certificate", + Action: run, + }, + { + Name: "auth", + Usage: "Create a certificate", + Action: func(c *cli.Context) { + logger().Fatal("Not implemented") + }, + }, + { + Name: "install", + Usage: "Install a certificate", + Action: func(c *cli.Context) { + logger().Fatal("Not implemented") + }, + }, + { + Name: "revoke", + Usage: "Revoke a certificate", + Action: func(c *cli.Context) { + logger().Fatal("Not implemented") + }, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "certificate", + Usage: "Revoke a specific certificate", + }, + cli.StringFlag{ + Name: "key", + Usage: "Revoke all certs generated by the provided authorized key.", + }, + }, + }, + { + Name: "rollback", + Usage: "Rollback a certificate", + Action: func(c *cli.Context) { + logger().Fatal("Not implemented") + }, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "checkpoints", + Usage: "Revert configuration N number of checkpoints", + }, + }, + }, + } + + app.Flags = []cli.Flag{ + cli.StringSliceFlag{ + Name: "domains, d", + Usage: "Add domains to the process", + }, + cli.StringFlag{ + Name: "server, s", + Value: "https://www.letsencrypt-demo.org/acme/new-reg", + Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.", + }, + cli.StringFlag{ + Name: "authkey, k", + Usage: "Path to the authorized key file", + }, + cli.StringFlag{ + Name: "email, m", + Usage: "Email used for registration and recovery contact.", + }, + cli.IntFlag{ + Name: "rsa-key-size, B", + Value: 2048, + Usage: "Size of the RSA key.", + }, + cli.BoolFlag{ + Name: "no-confirm", + Usage: "Turn off confirmation screens.", + }, + cli.BoolFlag{ + Name: "agree-tos, e", + Usage: "Skip the end user license agreement screen.", + }, + cli.StringFlag{ + Name: "config-dir", + Value: configDir, + Usage: "Configuration directory.", + }, + cli.StringFlag{ + Name: "work-dir", + Value: workDir, + Usage: "Working directory.", + }, + cli.StringFlag{ + Name: "backup-dir", + Value: backupDir, + Usage: "Configuration backups directory.", + }, + cli.StringFlag{ + Name: "key-dir", + Value: keyDir, + Usage: "Keys storage.", + }, + cli.StringFlag{ + Name: "cert-dir", + Value: certDir, + Usage: "Certificates storage.", + }, + } + + app.Run(os.Args) +} + +func checkFolder(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return os.MkdirAll(path, 0700) + } + return nil +} + +func run(c *cli.Context) { + err := checkFolder(c.GlobalString("config-dir")) + if err != nil { + logger().Fatalf("Cound not check/create path: %v", err) + } + + conf := NewConfiguration(c) + + //TODO: move to account struct? Currently MUST pass email. + if !c.GlobalIsSet("email") { + logger().Fatal("You have to pass an account (email address) to the program using --email or -m") + } + + acc := NewAccount(c.GlobalString("email"), conf) + client := acme.NewClient(c.GlobalString("server"), acc) + if acc.Registration == nil { + reg, err := client.Register() + if err != nil { + logger().Fatalf("Could not complete registration -> %v", err) + } + + acc.Registration = reg + acc.Save() + + logger().Print("!!!! HEADS UP !!!!") + logger().Printf(` + Your account credentials have been saved in your Let's Encrypt + configuration directory at "%s". + You should make a secure backup of this folder now. This + configuration directory will also contain certificates and + private keys obtained from Let's Encrypt so making regular + backups of this folder is ideal. + + If you lose your account credentials, you can recover + them using the token + "%s". + You must write that down and put it in a safe place.`, c.GlobalString("config-dir"), reg.Body.Recoverytoken) + } + + if !c.GlobalIsSet("domains") { + logger().Fatal("Please specify --domains") + } + +} diff --git a/cli/main_unix.go b/cli/main_unix.go new file mode 100644 index 00000000..b9ac46c0 --- /dev/null +++ b/cli/main_unix.go @@ -0,0 +1,361 @@ +package cli + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "flag" + "io/ioutil" + "log" + "math/big" + "net/http" + "os" + "strings" + "time" + + "github.com/square/go-jose" +) + +var ( + newReg = flag.String("new-reg", "http://192.168.10.22:4000/acme/new-reg", "New Registration URL") + accountKeyFile = flag.String("certKey", "account.key", "Private key file. Created if it does not exist.") + accountTmpCrtFile = flag.String("acc-crt-file", "account-tmp.pem", "Temporary self signed certificate for challenges.") + + email = flag.String("email", "", "Email address used for certificate retrieval.") + domain = flag.String("domain", "", "The domain to request a certificate for") + + ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521") + bits = flag.Int("bits", 2048, "The size of the RSA keys in bits. Default 4096") +) + +// Logger is used to log errors; if nil, the default log.Logger is used. +var Logger *log.Logger + +// logger is an helper function to retrieve the available logger +func logger() *log.Logger { + if Logger == nil { + Logger = log.New(os.Stderr, "", log.LstdFlags) + } + return Logger +} + +type Registration struct { + Contact []string `json:"contact"` +} + +type Tos struct { + Agreement string `json:"agreement"` +} + +type GetChallenges struct { + Identifier `json:"identifier"` +} + +type Identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type ChallengesResponse struct { + Identifier `json:"identifier"` + Status string `json:"status"` + Expires time.Time `json:"expires"` + Challenges []Challenge `json:"challenges"` + Combinations [][]int `json:"combinations"` +} + +type Challenge struct { + Type string `json:"type"` + Status string `json:"status"` + URI string `json:"uri"` + Token string `json:"token"` +} + +type SimpleHttpsMessage struct { + Path string `json:"path"` +} + +type CsrMessage struct { + Csr string `json:"csr"` + Authorizations []string `json:"authorizations"` +} + +func execute() { + flag.Parse() + accountKey := generateKeyPair(*accountKeyFile).(*rsa.PrivateKey) + jsonBytes, _ := json.Marshal(Registration{Contact: []string{"mailto:" + *email}}) + logger().Printf("Posting registration to %s", *newReg) + + resp, _ := jwsPost(*newReg, jsonBytes) + + links := parseLinks(resp.Header["Link"]) + if links["next"] == "" { + logger().Fatalln("The server did not provide enough information to proceed.") + } + + logger().Printf("Got agreement URL: %s", links["terms-of-service"]) + logger().Printf("Got new auth URL: %s", links["next"]) + + jsonBytes, _ = json.Marshal(Tos{Agreement: links["terms-of-service"]}) + logger().Printf("Posting agreement to %s", resp.Header.Get("Location")) + + resp, _ = jwsPost(resp.Header.Get("Location"), jsonBytes) + logResponse(resp) + + jsonBytes, _ = json.Marshal(GetChallenges{Identifier{Type: "dns", Value: *domain}}) + logger().Printf("Getting challenges for type %s and domain %s", "dns", *domain) + + resp, _ = jwsPost(links["next"], jsonBytes) + logResponse(resp) + + links = parseLinks(resp.Header["Link"]) + if links["next"] == "" { + logger().Fatalln("The server did not provide enough information to proceed.") + } + + logger().Printf("Got new cert URL: %s", links["next"]) + logger().Printf("Got new authorization URL: %s", resp.Header.Get("Location")) + newCertUrl := links["next"] + authUrl := resp.Header.Get("Location") + body, _ := ioutil.ReadAll(resp.Body) + + var challenges ChallengesResponse + _ = json.Unmarshal(body, &challenges) + + for _, challenge := range challenges.Challenges { + logger().Printf("Got challenge %s", challenge.Type) + } + logger().Printf("Challenge combinations are: %v", challenges.Combinations) + logger().Printf("Choosing first challenge combination and starting with %s", challenges.Challenges[challenges.Combinations[0][0]].Type) + + firstChallenge := challenges.Challenges[challenges.Combinations[0][0]] + if firstChallenge.Type == "simpleHttps" { + generateSelfSignedCert(accountKey) + + path := getRandomString(8) + ".txt" + challengePath := "/.well-known/acme-challenge/" + path + startChallengeTlsServer(challengePath, firstChallenge.Token) + + logger().Print("Waiting for domain validation...") + + jsonBytes, _ = json.Marshal(SimpleHttpsMessage{Path: path}) + logger().Printf("Sending challenge response for path %s", path) + + resp, _ = jwsPost(firstChallenge.URI, jsonBytes) + logResponse(resp) + + // Loop until status is verified or error. + var challengeResponse Challenge + loop: + for { + decoder := json.NewDecoder(resp.Body) + decoder.Decode(&challengeResponse) + + switch challengeResponse.Status { + case "valid": + logger().Print("The CA validated our credentials. Continue...") + break loop + case "pending": + logger().Print("The data is still being validated. Please stand by...") + case "invalid": + logger().Fatalf("The CA could not validate the provided file. - %v", challengeResponse) + default: + logger().Fatalf("The CA returned an unexpected state. - %v", challengeResponse) + } + + time.Sleep(1000 * time.Millisecond) + resp, _ = http.Get(authUrl) + } + } + + logger().Print("Getting certificate...") + privateSslKey := generateKeyPair("ssl-priv.key") + csr := generateCsr(privateSslKey) + csrString := base64.URLEncoding.EncodeToString(csr) + jsonBytes, _ = json.Marshal(CsrMessage{Csr: csrString, Authorizations: []string{authUrl}}) + resp, _ = jwsPost(newCertUrl, jsonBytes) + logResponse(resp) + + body, _ = ioutil.ReadAll(resp.Body) + ioutil.WriteFile("ssl-crt.crt", body, 0644) +} + +func startChallengeTlsServer(path string, token string) { + + cert, err := tls.LoadX509KeyPair(*accountTmpCrtFile, *accountKeyFile) + tlsConf := new(tls.Config) + tlsConf.Certificates = []tls.Certificate{cert} + + tlsListener, err := tls.Listen("tcp", ":443", tlsConf) + if err != nil { + logger().Fatalf("Could not start TLS listener on %s for challenge handling! - %v", ":443", err) + } + + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + if r.Host == *domain && r.Method == "GET" { + w.Write([]byte(token)) + tlsListener.Close() + } + }) + + srv := http.Server{Addr: ":443", Handler: nil} + go func() { + srv.Serve(tlsListener) + logger().Print("TLS Server exited.") + }() +} + +func getRandomString(length int) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, length) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + return string(bytes) +} + +func logResponse(resp *http.Response) { + logger().Println(resp.Status) + for k, v := range resp.Header { + logger().Printf("-- %s: %s", k, v) + } +} + +func jwsPost(url string, content []byte) (*http.Response, error) { + url = strings.Replace(url, "localhost", "192.168.10.22", -1) + + privKeyBytes, err := ioutil.ReadFile(*accountKeyFile) + key, err := jose.LoadPrivateKey(privKeyBytes) + if err != nil { + panic(err) + } + + signer, err := jose.NewSigner(jose.RS256, key) + if err != nil { + panic(err) + } + signed, err := signer.Sign(content) + if err != nil { + panic(err) + } + signedContent := signed.FullSerialize() + + resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(signedContent))) + if err != nil { + logger().Fatalf("Error posting content: %s", err) + } + + return resp, err +} + +func generateCsr(privateKey interface{}) []byte { + template := x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: *domain, + }, + EmailAddresses: []string{*email}, + } + + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, privateKey) + + csrOut, err := os.Create("csr.pem") + if err != nil { + logger().Fatalf("Could not create certificate request file: %s", err) + } + + pem.Encode(csrOut, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) + + return csrBytes +} + +func generateKeyPair(fileName string) interface{} { + logger().Println("Generating key pair ...") + + var privateKey interface{} + var err error + switch *ecdsaCurve { + case "": + privateKey, err = rsa.GenerateKey(rand.Reader, *bits) + case "P224": + privateKey, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + case "P256": + privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case "P384": + privateKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case "P521": + privateKey, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + } + + if err != nil { + logger().Fatalf("Failed to generate private key: %s", err) + } + + var pemKey pem.Block + switch key := privateKey.(type) { + case *rsa.PrivateKey: + pemKey = pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} + case *ecdsa.PrivateKey: + privateBytes, err := x509.MarshalECPrivateKey(key) + if err != nil { + logger().Fatalf("Could not marshal ECDSA private key: %v", err) + } + + pemKey = pem.Block{Type: "EC PRIVATE KEY", Bytes: privateBytes} + } + + certOut, err := os.Create(fileName) + if err != nil { + logger().Fatalf("Could not create private key file: %s", err) + } + + pem.Encode(certOut, &pemKey) + certOut.Close() + + return privateKey +} + +func generateSelfSignedCert(privKey *rsa.PrivateKey) { + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("failed to generate serial number: %s", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: *email, + Organization: []string{*domain}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{*domain}, + IsCA: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + log.Fatalf("Failed to create certificate: %s", err) + } + + certOut, err := os.Create(*accountTmpCrtFile) + if err != nil { + log.Fatalf("failed to open cert.pem for writing: %s", err) + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() +} diff --git a/configuration.go b/configuration.go new file mode 100644 index 00000000..75760ea1 --- /dev/null +++ b/configuration.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/url" + "os" + "path" + "strings" + + "github.com/codegangsta/cli" +) + +// Configuration type from CLI and config files. +type Configuration struct { + context *cli.Context +} + +// NewConfiguration creates a new configuration from CLI data. +func NewConfiguration(c *cli.Context) *Configuration { + return &Configuration{context: c} +} + +// RsaBits returns the current set RSA bit length for private keys +func (c *Configuration) RsaBits() int { + return c.context.GlobalInt("rsa-key-size") +} + +// ServerPath returns the OS dependent path to the data for a specific CA +func (c *Configuration) ServerPath() string { + srv, _ := url.Parse(c.context.GlobalString("server")) + srvStr := strings.Replace(srv.Host, ":", "_", -1) + srv.Path + return strings.Replace(srvStr, "/", string(os.PathSeparator), -1) +} + +// AccountsPath returns the OS dependent path to the +// local accounts for a specific CA +func (c *Configuration) AccountsPath() string { + return path.Join(c.context.GlobalString("config-dir"), "accounts", c.ServerPath()) +} + +// AccountPath returns the OS dependent path to a particular account +func (c *Configuration) AccountPath(acc string) string { + return path.Join(c.AccountsPath(), acc) +} + +// AccountPath returns the OS dependent path to the keys of a particular account +func (c *Configuration) AccountKeysPath(acc string) string { + return path.Join(c.AccountPath(acc), "keys") +} diff --git a/crypto.go b/crypto.go new file mode 100644 index 00000000..3644ed99 --- /dev/null +++ b/crypto.go @@ -0,0 +1,39 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" +) + +func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, length) + if err != nil { + return nil, err + } + + pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} + + certOut, err := os.Create(file) + if err != nil { + return nil, err + } + + pem.Encode(certOut, &pemKey) + certOut.Close() + + return privateKey, nil +} + +func loadRsaKey(file string) (*rsa.PrivateKey, error) { + keyBytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + keyBlock, _ := pem.Decode(keyBytes) + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) +} diff --git a/path_unix.go b/path_unix.go new file mode 100644 index 00000000..dedb388f --- /dev/null +++ b/path_unix.go @@ -0,0 +1,11 @@ +// +build !windows + +package main + +var ( + configDir = "/etc/letsencrypt" + workDir = "/var/lib/letsencrypt" + backupDir = "/var/lib/letsencrypt/backups" + keyDir = "/etc/letsencrypt/keys" + certDir = "/etc/letsencrypt/certs" +) diff --git a/path_windows.go b/path_windows.go new file mode 100644 index 00000000..77c95b40 --- /dev/null +++ b/path_windows.go @@ -0,0 +1,13 @@ +// +build !linux + +package main + +import "os" + +var ( + configDir = os.ExpandEnv("${PROGRAMDATA}\\letsencrypt") + workDir = os.ExpandEnv("${PROGRAMDATA}\\letsencrypt") + backupDir = os.ExpandEnv("${PROGRAMDATA}\\letsencrypt\\backups") + keyDir = os.ExpandEnv("${PROGRAMDATA}\\letsencrypt\\keys") + certDir = os.ExpandEnv("${PROGRAMDATA}\\letsencrypt\\certs") +)