Commit 42ac2d2d authored by Matthew Holt's avatar Matthew Holt

letsencrypt: More tests, tests for user.go & slight refactoring

parent d7641118
...@@ -5,20 +5,22 @@ import ( ...@@ -5,20 +5,22 @@ import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem"
"os" "os"
"testing" "testing"
) )
func init() {
rsaKeySizeToUse = 128 // makes tests faster
}
func TestSaveAndLoadRSAPrivateKey(t *testing.T) { func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
keyFile := "test.key" keyFile := "test.key"
defer os.Remove(keyFile) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
privateKeyPEM := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
// test save // test save
err = saveRSAPrivateKey(privateKey, keyFile) err = saveRSAPrivateKey(privateKey, keyFile)
...@@ -31,10 +33,19 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) { ...@@ -31,10 +33,19 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
if err != nil { if err != nil {
t.Error("error loading private key:", err) t.Error("error loading private key:", err)
} }
loadedKeyPEM := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(loadedKey)}
// very loaded key is correct // 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") 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))
}
// Package letsencrypt integrates Let's Encrypt with Caddy with first-class support. // Package letsencrypt integrates Let's Encrypt functionality into Caddy
// It is designed to configure sites for HTTPS by default. // with first-class support for creating and renewing certificates
// automatically. It is designed to configure sites for HTTPS by default.
package letsencrypt package letsencrypt
import ( import (
...@@ -126,7 +127,7 @@ func newClient(leEmail string) (*acme.Client, error) { ...@@ -126,7 +127,7 @@ func newClient(leEmail string) (*acme.Client, error) {
} }
// The client facilitates our communication with the CA server. // 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 // If not registered, the user must register an account with the CA
// and agree to terms // and agree to terms
...@@ -268,9 +269,6 @@ var ( ...@@ -268,9 +269,6 @@ var (
// Some essential values related to the Let's Encrypt process // Some essential values related to the Let's Encrypt process
const ( const (
// Size of RSA keys in bits
rsaKeySize = 2048
// The base URL to the Let's Encrypt CA // The base URL to the Let's Encrypt CA
caURL = "http://192.168.99.100:4000" caURL = "http://192.168.99.100:4000"
...@@ -278,10 +276,10 @@ const ( ...@@ -278,10 +276,10 @@ const (
exposePort = "5001" exposePort = "5001"
) )
// KeySize represents the length of a key in bits // KeySize represents the length of a key in bits.
type KeySize int type KeySize int
// Key sizes // Key sizes are used to determine the strength of a key.
const ( const (
ECC_224 KeySize = 224 ECC_224 KeySize = 224
ECC_256 = 256 ECC_256 = 256
...@@ -289,6 +287,13 @@ const ( ...@@ -289,6 +287,13 @@ const (
RSA_4096 = 4096 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 { type CertificateMeta struct {
Domain, URL string Domain, URL string
} }
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
...@@ -15,6 +16,7 @@ import ( ...@@ -15,6 +16,7 @@ import (
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
) )
// User represents a Let's Encrypt user account.
type User struct { type User struct {
Email string Email string
Registration *acme.RegistrationResource Registration *acme.RegistrationResource
...@@ -22,18 +24,25 @@ type User struct { ...@@ -22,18 +24,25 @@ type User struct {
key *rsa.PrivateKey key *rsa.PrivateKey
} }
// GetEmail gets u's email.
func (u User) GetEmail() string { func (u User) GetEmail() string {
return u.Email return u.Email
} }
// GetRegistration gets u's registration resource.
func (u User) GetRegistration() *acme.RegistrationResource { func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration return u.Registration
} }
// GetPrivateKey gets u's private key.
func (u User) GetPrivateKey() *rsa.PrivateKey { func (u User) GetPrivateKey() *rsa.PrivateKey {
return u.key return u.key
} }
// getUser loads the user with the given email from disk. // 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) { func getUser(email string) (User, error) {
var user User var user User
...@@ -95,7 +104,7 @@ func saveUser(user User) error { ...@@ -95,7 +104,7 @@ func saveUser(user User) error {
// instead. // instead.
func newUser(email string) (User, error) { func newUser(email string) (User, error) {
user := User{Email: email} user := User{Email: email}
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
if err != nil { if err != nil {
return user, errors.New("error generating private key: " + err.Error()) return user, errors.New("error generating private key: " + err.Error())
} }
...@@ -134,7 +143,8 @@ func getEmail(cfg server.Config) string { ...@@ -134,7 +143,8 @@ func getEmail(cfg server.Config) string {
} }
if leEmail == "" { if leEmail == "" {
// Alas, we must bother the user and ask for an email address // 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? fmt.Print("Email address: ") // TODO: More explanation probably, and show ToS?
var err error var err error
leEmail, err = reader.ReadString('\n') leEmail, err = reader.ReadString('\n')
...@@ -145,3 +155,7 @@ func getEmail(cfg server.Config) string { ...@@ -145,3 +155,7 @@ func getEmail(cfg server.Config) string {
} }
return strings.TrimSpace(leEmail) 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)
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)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment