mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-22 10:25:46 +03:00
letsencrypt: More tests, tests for user.go & slight refactoring
This commit is contained in:
parent
d764111886
commit
42ac2d2dde
4 changed files with 238 additions and 16 deletions
|
@ -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) {
|
||||||
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))
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// 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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (
|
||||||
|
|
||||||
// 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 (
|
||||||
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 (
|
||||||
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 (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
}
|
}
|
||||||
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 {
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|
192
config/letsencrypt/user_test.go
Normal file
192
config/letsencrypt/user_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue