Skip to content
Snippets Groups Projects
server_test.go 3.95 KiB
Newer Older
ale's avatar
ale committed
package acmeserver

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
ale's avatar
ale committed
	"crypto/x509"
	"crypto/x509/pkix"
ale's avatar
ale committed
	"io/ioutil"
	"log"
	"math/big"
ale's avatar
ale committed
	"os"
	"path/filepath"
	"testing"
	"time"
)

type fakeACME struct {
}

func (f *fakeACME) GetCertificate(_ context.Context, priv crypto.Signer, domains []string) ([][]byte, *x509.Certificate, error) {
	notBefore := time.Now()
	notAfter := notBefore.AddDate(1, 0, 0)

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	if err != nil {
		return nil, nil, err
	}

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			CommonName: domains[0],
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	if len(domains) > 1 {
		template.DNSNames = domains[1:]
	}

	der, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
	if err != nil {
		return nil, nil, err
	}

	cert, err := x509.ParseCertificate(der)
	if err != nil {
		return nil, nil, err
	}

	log.Printf("created certificate for %s", domains[0])

	return [][]byte{der}, cert, nil
ale's avatar
ale committed
}

func publicKey(priv interface{}) interface{} {
	switch k := priv.(type) {
	case *rsa.PrivateKey:
		return &k.PublicKey
	case *ecdsa.PrivateKey:
		return &k.PublicKey
	default:
		return nil
	}
}

func newTestManager(t testing.TB) (func(), *Manager) {
ale's avatar
ale committed
	dir, err := ioutil.TempDir("", "")
	if err != nil {
		t.Fatal(err)
	}
ale's avatar
ale committed
	os.Mkdir(filepath.Join(dir, "config"), 0700)
	ioutil.WriteFile(
		filepath.Join(dir, "config", "test.yml"),
		[]byte("- { cn: example.com }\n"),
		0644,
	)

	m, err := NewManager(&Config{
		Dir:   dir,
		Email: "test@example.com",
	}, &fakeACME{})
	if err != nil {
		t.Fatal(err)
	}

	if err := m.Start(context.Background()); err != nil {
		t.Fatal("Start:", err)
	}

	// Wait just a little bit to give a chance to m.loop() to run.
	time.Sleep(50 * time.Millisecond)

	return func() {
		m.Stop()
		os.RemoveAll(dir)
	}, m
}

func TestManager_Reload(t *testing.T) {
	cleanup, m := newTestManager(t)
	defer cleanup()

ale's avatar
ale committed
	// Data race: we read data owned by another goroutine!
	if len(m.certs) < 1 {
		t.Fatal("configuration not loaded?")
	}
	if m.certs[0].domains[0] != "example.com" {
		t.Fatalf("certs[0].domains[0] is %s, expected example.com", m.certs[0].domains[0])
	}

	// Try a reload, catch obvious errors.
	m.Reload()
	time.Sleep(50 * time.Millisecond)

	if len(m.certs) != 1 {
		t.Fatalf("certs count is %d, expected 1", len(m.certs))
	}
	if m.certs[0].domains[0] != "example.com" {
		t.Fatalf("certs[0].domains[0] is %s, expected example.com", m.certs[0].domains[0])
	}
}

func TestManager_NewCert(t *testing.T) {
	cleanup, m := newTestManager(t)
	defer cleanup()

	now := time.Now()
	ci := m.certs[0]
	if ci.retryDeadline.After(now) {
		t.Fatalf("retry deadline is in the future: %v", ci.retryDeadline)
	}

	testUpdateCh <- true
	time.Sleep(100 * time.Millisecond)

	// Verify that the retry/renewal timestamp is in the future.
	if ci.retryDeadline.Before(now) {
		t.Fatalf("retry deadline is in the past after renewal: %v", ci.retryDeadline)
	}

	// Do we think we have a valid certificate?
	if !ci.valid {
		t.Fatal("we don't have a valid certificate")
	}

	// Verify that the credentials have successfully been written
	// to storage.
	p := filepath.Join(m.configDir, "../certs/example.com/cert.pem")
	if _, err := os.Stat(p); err != nil {
		t.Fatalf("file not created: %v", err)
	}
	p = filepath.Join(m.configDir, "../certs/example.com/private_key.pem")
	if _, err := os.Stat(p); err != nil {
		t.Fatalf("file not created: %v", err)
	}

	// By triggering a reload now, we should cause the Manager to
	// reload the certificate from storage.
	m.Reload()
	time.Sleep(50 * time.Millisecond)

	ci = m.certs[0]
	if !ci.valid {
		t.Fatal("certificate is invalid after a reload")
	}
}