From 42ac2d2dde81b8ff92aae80e93432b10bd9b0dca Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 18 Oct 2015 12:09:06 -0600 Subject: [PATCH] letsencrypt: More tests, tests for user.go & slight refactoring --- config/letsencrypt/crypto_test.go | 21 +++- config/letsencrypt/letsencrypt.go | 21 ++-- config/letsencrypt/user.go | 20 +++- config/letsencrypt/user_test.go | 192 ++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 config/letsencrypt/user_test.go diff --git a/config/letsencrypt/crypto_test.go b/config/letsencrypt/crypto_test.go index c99c54c1..938778a8 100644 --- a/config/letsencrypt/crypto_test.go +++ b/config/letsencrypt/crypto_test.go @@ -5,20 +5,22 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" - "encoding/pem" "os" "testing" ) +func init() { + rsaKeySizeToUse = 128 // makes tests faster +} + func TestSaveAndLoadRSAPrivateKey(t *testing.T) { keyFile := "test.key" defer os.Remove(keyFile) - privateKey, err := rsa.GenerateKey(rand.Reader, 256) // small key size is OK for testing + privateKey, err := rsa.GenerateKey(rand.Reader, 128) // small key size is OK for testing if err != nil { t.Fatal(err) } - privateKeyPEM := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} // test save err = saveRSAPrivateKey(privateKey, keyFile) @@ -31,10 +33,19 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) { if err != nil { t.Error("error loading private key:", err) } - loadedKeyPEM := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(loadedKey)} // very loaded key is correct - if !bytes.Equal(loadedKeyPEM.Bytes, privateKeyPEM.Bytes) { + if !rsaPrivateKeysSame(privateKey, loadedKey) { t.Error("Expected key bytes to be the same, but they weren't") } } + +// rsaPrivateKeyBytes returns the bytes of DER-encoded key. +func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte { + return x509.MarshalPKCS1PrivateKey(key) +} + +// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same. +func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool { + return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b)) +} diff --git a/config/letsencrypt/letsencrypt.go b/config/letsencrypt/letsencrypt.go index dd968090..58dfcd78 100644 --- a/config/letsencrypt/letsencrypt.go +++ b/config/letsencrypt/letsencrypt.go @@ -1,5 +1,6 @@ -// Package letsencrypt integrates Let's Encrypt with Caddy with first-class support. -// It is designed to configure sites for HTTPS by default. +// Package letsencrypt integrates Let's Encrypt functionality into Caddy +// with first-class support for creating and renewing certificates +// automatically. It is designed to configure sites for HTTPS by default. package letsencrypt import ( @@ -126,7 +127,7 @@ func newClient(leEmail string) (*acme.Client, error) { } // The client facilitates our communication with the CA server. - client := acme.NewClient(caURL, &leUser, rsaKeySize, exposePort, true) // TODO: Dev mode is enabled + client := acme.NewClient(caURL, &leUser, rsaKeySizeToUse, exposePort, true) // TODO: Dev mode is enabled // If not registered, the user must register an account with the CA // and agree to terms @@ -268,9 +269,6 @@ var ( // Some essential values related to the Let's Encrypt process const ( - // Size of RSA keys in bits - rsaKeySize = 2048 - // The base URL to the Let's Encrypt CA caURL = "http://192.168.99.100:4000" @@ -278,10 +276,10 @@ const ( exposePort = "5001" ) -// KeySize represents the length of a key in bits +// KeySize represents the length of a key in bits. type KeySize int -// Key sizes +// Key sizes are used to determine the strength of a key. const ( ECC_224 KeySize = 224 ECC_256 = 256 @@ -289,6 +287,13 @@ const ( RSA_4096 = 4096 ) +// rsaKeySizeToUse is the size to use for new RSA keys. +// This shouldn't need to change except for in tests; +// the size can be drastically reduced for speed. +var rsaKeySizeToUse = RSA_2048 + +// CertificateMeta is a container type used to write out a file +// with information about a certificate. type CertificateMeta struct { Domain, URL string } diff --git a/config/letsencrypt/user.go b/config/letsencrypt/user.go index f66acee4..752cc510 100644 --- a/config/letsencrypt/user.go +++ b/config/letsencrypt/user.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "os" "strings" @@ -15,6 +16,7 @@ import ( "github.com/xenolf/lego/acme" ) +// User represents a Let's Encrypt user account. type User struct { Email string Registration *acme.RegistrationResource @@ -22,18 +24,25 @@ type User struct { key *rsa.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() *rsa.PrivateKey { return u.key } // getUser loads the user with the given email from disk. -// If the user does not exist, it will create a new one. +// 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. func getUser(email string) (User, error) { var user User @@ -95,7 +104,7 @@ func saveUser(user User) error { // instead. func newUser(email string) (User, error) { user := User{Email: email} - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) + privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse) if err != nil { return user, errors.New("error generating private key: " + err.Error()) } @@ -134,7 +143,8 @@ func getEmail(cfg server.Config) string { } if leEmail == "" { // Alas, we must bother the user and ask for an email address - reader := bufio.NewReader(os.Stdin) + // TODO/BUG: This doesn't work when Caddyfile is piped into caddy + reader := bufio.NewReader(stdin) fmt.Print("Email address: ") // TODO: More explanation probably, and show ToS? var err error leEmail, err = reader.ReadString('\n') @@ -145,3 +155,7 @@ func getEmail(cfg server.Config) string { } return strings.TrimSpace(leEmail) } + +// stdin is used to read the user's input if prompted; +// this is changed by tests during tests. +var stdin = io.ReadWriter(os.Stdin) diff --git a/config/letsencrypt/user_test.go b/config/letsencrypt/user_test.go new file mode 100644 index 00000000..d074856a --- /dev/null +++ b/config/letsencrypt/user_test.go @@ -0,0 +1,192 @@ +package letsencrypt + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/mholt/caddy/server" + "github.com/xenolf/lego/acme" +) + +func TestUser(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 128) + if err != nil { + t.Fatalf("Could not generate test private key: %v", err) + } + u := User{ + Email: "me@mine.com", + Registration: new(acme.RegistrationResource), + key: privateKey, + } + + if expected, actual := "me@mine.com", u.GetEmail(); actual != expected { + t.Errorf("Expected email '%s' but got '%s'", expected, actual) + } + if u.GetRegistration() == nil { + t.Error("Expected a registration resource, but got nil") + } + if expected, actual := privateKey, u.GetPrivateKey(); actual != expected { + t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual) + } +} + +func TestNewUser(t *testing.T) { + email := "me@foobar.com" + user, err := newUser(email) + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + if user.key == nil { + t.Error("Private key is nil") + } + if user.Email != email { + t.Errorf("Expected email to be %s, but was %s", email, user.Email) + } + if user.Registration != nil { + t.Error("New user already has a registration resource; it shouldn't") + } +} + +func TestSaveUser(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + + email := "me@foobar.com" + user, err := newUser(email) + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + + err = saveUser(user) + if err != nil { + t.Fatalf("Error saving user: %v", err) + } + _, err = os.Stat(storage.UserRegFile(email)) + if err != nil { + t.Errorf("Cannot access user registration file, error: %v", err) + } + _, err = os.Stat(storage.UserKeyFile(email)) + if err != nil { + t.Errorf("Cannot access user private key file, error: %v", err) + } +} + +func TestGetUserDoesNotAlreadyExist(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + + user, err := getUser("user_does_not_exist@foobar.com") + if err != nil { + t.Fatalf("Error getting user: %v", err) + } + + if user.key == nil { + t.Error("Expected user to have a private key, but it was nil") + } +} + +func TestGetUserAlreadyExists(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + + email := "me@foobar.com" + + // Set up test + user, err := newUser(email) + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + err = saveUser(user) + if err != nil { + t.Fatalf("Error saving user: %v", err) + } + + // Expect to load user from disk + user2, err := getUser(email) + if err != nil { + t.Fatalf("Error getting user: %v", err) + } + + // Assert keys are the same + if !rsaPrivateKeysSame(user.key, user2.key) { + t.Error("Expected private key to be the same after loading, but it wasn't") + } + + // Assert emails are the same + if user.Email != user2.Email { + t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email) + } +} + +func TestGetEmail(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + DefaultEmail = "test2@foo.com" + + // Test1: Use email in config + config := server.Config{ + TLS: server.TLSConfig{ + LetsEncryptEmail: "test1@foo.com", + }, + } + actual := getEmail(config) + if actual != "test1@foo.com" { + t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "test1@foo.com", actual) + } + + // Test2: Use default email from flag (or user previously typing it) + actual = getEmail(server.Config{}) + if actual != DefaultEmail { + t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual) + } + + // Test3: Get input from user + DefaultEmail = "" + stdin = new(bytes.Buffer) + _, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n")) + if err != nil { + t.Fatalf("Could not simulate user input, error: %v", err) + } + actual = getEmail(server.Config{}) + if actual != "test3@foo.com" { + t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual) + } + + // Test4: Get most recent email from before + DefaultEmail = "" + for i, eml := range []string{ + "test4-3@foo.com", + "test4-2@foo.com", + "test4-1@foo.com", + } { + u, err := newUser(eml) + if err != nil { + t.Fatalf("Error creating user %d: %v", i, err) + } + err = saveUser(u) + if err != nil { + t.Fatalf("Error saving user %d: %v", i, err) + } + + // Change modified time so they're all different, so the test becomes deterministic + f, err := os.Stat(storage.User(eml)) + if err != nil { + t.Fatalf("Could not access user folder for '%s': %v", eml, err) + } + chTime := f.ModTime().Add(-(time.Duration(i) * time.Second)) + if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil { + t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err) + } + } + + actual = getEmail(server.Config{}) + if actual != "test4-3@foo.com" { + t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual) + } +}