// Copyright 2015 Light Code Labs, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caddytls import ( "bufio" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "encoding/json" "errors" "fmt" "io" "os" "strings" "github.com/xenolf/lego/acmev2" ) // User represents a Let's Encrypt user account. type User struct { Email string Registration *acme.RegistrationResource key crypto.PrivateKey } // GetEmail gets u's email. func (u User) GetEmail() string { return u.Email } // GetRegistration gets u's registration resource. func (u User) GetRegistration() *acme.RegistrationResource { return u.Registration } // GetPrivateKey gets u's private key. func (u User) GetPrivateKey() crypto.PrivateKey { return u.key } // newUser creates a new User for the given email address // with a new private key. This function does NOT save the // user to disk or register it via ACME. If you want to use // a user account that might already exist, call getUser // instead. It does NOT prompt the user. func newUser(email string) (User, error) { user := User{Email: email} privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { return user, errors.New("error generating private key: " + err.Error()) } user.key = privateKey return user, nil } // getEmail does everything it can to obtain an email address // from the user within the scope of memory and storage to use // for ACME TLS. If it cannot get an email address, it returns // empty string. (If user is present, it will warn the user of // the consequences of an empty email.) This function MAY prompt // the user for input. If userPresent is false, the operator // will NOT be prompted and an empty email may be returned. // If the user is prompted, a new User will be created and // stored in storage according to the email address they // provided (which might be blank). func getEmail(cfg *Config, userPresent bool) (string, error) { storage, err := cfg.StorageFor(cfg.CAUrl) if err != nil { return "", err } // First try memory (command line flag or typed by user previously) leEmail := DefaultEmail // Then try to get most recent user email from storage if leEmail == "" { leEmail = storage.MostRecentUserEmail() DefaultEmail = leEmail // save for next time } // Looks like there is no email address readily available, // so we will have to ask the user if we can. if leEmail == "" && userPresent { // evidently, no User data was present in storage; // thus we must make a new User so that we can get // the Terms of Service URL via our ACME client, phew! user, err := newUser("") if err != nil { return "", err } // get the agreement URL agreementURL := agreementTestURL if agreementURL == "" { // we call acme.NewClient directly because newACMEClient // would require that we already know the user's email caURL := DefaultCAUrl if cfg.CAUrl != "" { caURL = cfg.CAUrl } tempClient, err := acme.NewClient(caURL, user, "") if err != nil { return "", fmt.Errorf("making ACME client to get ToS URL: %v", err) } agreementURL = tempClient.GetToSURL() } // prompt the user for an email address and terms agreement reader := bufio.NewReader(stdin) promptUserAgreement(agreementURL) fmt.Println("Please enter your email address to signify agreement and to be notified") fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.") fmt.Print(" Email address: ") leEmail, err = reader.ReadString('\n') if err != nil && err != io.EOF { return "", fmt.Errorf("reading email address: %v", err) } leEmail = strings.TrimSpace(leEmail) DefaultEmail = leEmail Agreed = true // save the new user to preserve this for next time user.Email = leEmail err = saveUser(storage, user) if err != nil { return "", err } } // lower-casing the email is important for consistency return strings.ToLower(leEmail), nil } // getUser loads the user with the given email from disk // using the provided storage. If the user does not exist, // it will create a new one, but it does NOT save new // users to the disk or register them via ACME. It does // NOT prompt the user. func getUser(storage Storage, email string) (User, error) { var user User // open user reg userData, err := storage.LoadUser(email) if err != nil { if _, ok := err.(ErrNotExist); ok { // create a new user return newUser(email) } return user, err } // load user information err = json.Unmarshal(userData.Reg, &user) if err != nil { return user, err } // load their private key user.key, err = loadPrivateKey(userData.Key) return user, err } // saveUser persists a user's key and account registration // to the file system. It does NOT register the user via ACME // or prompt the user. You must also pass in the storage // wherein the user should be saved. It should be the storage // for the CA with which user has an account. func saveUser(storage Storage, user User) error { // Save the private key and registration userData := new(UserData) var err error userData.Key, err = savePrivateKey(user.key) if err == nil { userData.Reg, err = json.MarshalIndent(&user, "", "\t") } if err == nil { err = storage.StoreUser(user.Email, userData) } return err } // promptUserAgreement simply outputs the standard user // agreement prompt with the given agreement URL. // It outputs a newline after the message. func promptUserAgreement(agreementURL string) { const userAgreementPrompt = `Your sites will be served over HTTPS automatically using Let's Encrypt. By continuing, you agree to the Let's Encrypt Subscriber Agreement at:` fmt.Printf("\n\n%s\n %s\n", userAgreementPrompt, agreementURL) } // askUserAgreement prompts the user to agree to the agreement // at the given agreement URL via stdin. It returns whether the // user agreed or not. func askUserAgreement(agreementURL string) bool { promptUserAgreement(agreementURL) fmt.Print("Do you agree to the terms? (y/n): ") reader := bufio.NewReader(stdin) answer, err := reader.ReadString('\n') if err != nil { return false } answer = strings.ToLower(strings.TrimSpace(answer)) return answer == "y" || answer == "yes" } // agreementTestURL is set during tests to skip requiring // setting up an entire ACME CA endpoint. var agreementTestURL string // stdin is used to read the user's input if prompted; // this is changed by tests during tests. var stdin = io.ReadWriter(os.Stdin) // The name of the folder for accounts where the email // address was not provided; default 'username' if you will, // but only for local/storage use, not with the CA. const emptyEmail = "default"