package acmeserver import ( "context" "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "io/ioutil" "log" "math/big" "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 } 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) { dir, err := ioutil.TempDir("", "") if err != nil { t.Fatal(err) } 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() // 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") } }