package server import ( "context" "errors" "net/http/httptest" "strings" "testing" "time" ) // failingIssuer returns an error immediately, for renewal-failure tests. type failingIssuer struct{ reason string } func (f *failingIssuer) Instant() bool { return false } func (f *failingIssuer) Issue(context.Context, string, string) (IssuedCert, error) { return IssuedCert{}, errors.New(f.reason) } // blockingIssuer signals when an order starts and waits for release. type blockingIssuer struct { started chan struct{} release chan struct{} result IssuedCert } func (b *blockingIssuer) Instant() bool { return false } func (b *blockingIssuer) Issue(ctx context.Context, _, _ string) (IssuedCert, error) { close(b.started) select { case <-b.release: return b.result, nil case <-ctx.Done(): return IssuedCert{}, ctx.Err() } } // fakeIssuer blocks until released, then returns a fixed result. type fakeIssuer struct { release chan struct{} result IssuedCert err error } func (f *fakeIssuer) Instant() bool { return false } func (f *fakeIssuer) Issue(ctx context.Context, csrPEM, hostname string) (IssuedCert, error) { select { case <-f.release: case <-ctx.Done(): return IssuedCert{}, ctx.Err() } return f.result, f.err } // countingIssuer records every Issue call and returns immediately. type countingIssuer struct { calls chan string } func (c *countingIssuer) Instant() bool { return false } func (c *countingIssuer) Issue(_ context.Context, _, hostname string) (IssuedCert, error) { c.calls <- hostname return IssuedCert{Certificate: "CERT2", Chain: "CHAIN2", Expiry: time.Now().Add(90 * 24 * time.Hour)}, nil } func TestAsyncIssuance(t *testing.T) { store, err := OpenStore("") if err != nil { t.Fatal(err) } issuer := &fakeIssuer{ release: make(chan struct{}), result: IssuedCert{Certificate: "CERT", Chain: "CHAIN", Expiry: time.Now().Add(90 * 24 * time.Hour)}, } api := &API{ Store: store, Hub: NewHub(store), Domain: "opentunnel.test", StartedAt: time.Now(), Issuer: issuer, IssuerName: "fake", } srv := httptest.NewServer(api) defer srv.Close() tunnel := createTunnel(t, srv) csr := makeCSR(t, tunnel.Hostname, tunnel.Hostname, "*."+tunnel.Hostname) resp := request(t, "POST", srv.URL+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "application/json", `{"csr":`+jsonString(csr)+`}`) if body := bodyString(t, resp); resp.StatusCode != 202 || !strings.Contains(body, `"type":"issuing"`) { t.Fatalf("bind: %d %q", resp.StatusCode, body) } // While the issuer holds, the certificate stays "issuing". resp = request(t, "GET", srv.URL+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "", "") if body := bodyString(t, resp); !strings.Contains(body, `"type":"issuing"`) { t.Fatalf("before release: %q", body) } close(issuer.release) deadline := time.Now().Add(3 * time.Second) var last string for time.Now().Before(deadline) { resp = request(t, "GET", srv.URL+"/api/tunnel/"+tunnel.ID+"/certificate", tunnel.Token, "", "") last = bodyString(t, resp) if strings.Contains(last, `"type":"ready"`) { if !strings.Contains(last, "CERT") || !strings.Contains(last, "CHAIN") { t.Errorf("ready certificate incomplete: %q", last) } return } time.Sleep(20 * time.Millisecond) } t.Fatalf("certificate never became ready; last: %q", last) } func TestRenewalMigratesForeignCertificates(t *testing.T) { store, err := OpenStore("") if err != nil { t.Fatal(err) } tunnel, _, err := store.createTunnel("opentunnel.test") if err != nil { t.Fatal(err) } // A ready certificate from the local CA that expires far in the future: // only the issuer mismatch should trigger a re-issue. if err := store.bindCertificate(tunnel.ID, storedCert{ ID: "cert_old", CSR: "csr-pem", Issuer: "local", State: "ready", Certificate: "OLDCERT", Chain: "OLDCHAIN", Expiry: expiryString(time.Now().Add(300 * 24 * time.Hour)), }); err != nil { t.Fatal(err) } issuer := &countingIssuer{calls: make(chan string, 4)} api := &API{Store: store, Hub: NewHub(store), Domain: "opentunnel.test", StartedAt: time.Now(), Issuer: issuer, IssuerName: "fake"} api.renewDue(false) // synchronous: the fake returns immediately select { case host := <-issuer.calls: if host != tunnel.Hostname { t.Errorf("issued for %q, want %q", host, tunnel.Hostname) } default: t.Fatal("expected a re-issue for the foreign certificate") } updated, _ := store.get(tunnel.ID) if updated.Certificate == nil || updated.Certificate.Certificate != "CERT2" || updated.Certificate.Issuer != "fake" { t.Errorf("certificate not replaced: %+v", updated.Certificate) } } func TestFailedRenewalKeepsServingCertificate(t *testing.T) { store, err := OpenStore("") if err != nil { t.Fatal(err) } tunnel, _, err := store.createTunnel("opentunnel.test") if err != nil { t.Fatal(err) } // Already inside the renewal window, from the current issuer. if err := store.bindCertificate(tunnel.ID, storedCert{ ID: "cert_old", CSR: "csr-pem", Issuer: "fake", State: "ready", Certificate: "OLDCERT", Chain: "OLDCHAIN", Expiry: expiryString(time.Now().Add(10 * 24 * time.Hour)), }); err != nil { t.Fatal(err) } api := &API{ Store: store, Hub: NewHub(store), Domain: "opentunnel.test", StartedAt: time.Now(), Issuer: &failingIssuer{reason: "dns unavailable"}, IssuerName: "fake", } api.renewDue(false) updated, _ := store.get(tunnel.ID) cert := updated.Certificate if cert == nil || cert.Certificate != "OLDCERT" || cert.Chain != "OLDCHAIN" || cert.Expiry == "" { t.Fatalf("failed renewal must keep the old certificate: %+v", cert) } if cert.State != "failed" || cert.Reason != "dns unavailable" { t.Errorf("failure not recorded: %+v", cert) } if got := certState(cert).Type; got != "ready" { t.Errorf("clients should still see the old certificate ready, got %q", got) } // The renewal loop retries a failed renewal. cert.State = "failed" api.renewDue(false) if updated, _ = store.get(tunnel.ID); updated.Certificate.Certificate != "OLDCERT" { t.Errorf("retry lost the old certificate: %+v", updated.Certificate) } } func TestStaleIssuanceDoesNotClobberNewBind(t *testing.T) { store, err := OpenStore("") if err != nil { t.Fatal(err) } tunnel, _, err := store.createTunnel("opentunnel.test") if err != nil { t.Fatal(err) } if err := store.bindCertificate(tunnel.ID, storedCert{ ID: "cert_a", CSR: "CSR_A", State: "issuing", Issuer: "fake", }); err != nil { t.Fatal(err) } issuer := &blockingIssuer{ started: make(chan struct{}), release: make(chan struct{}), result: IssuedCert{Certificate: "CERTS_A", Chain: "CHAIN_A", Expiry: time.Now().Add(90 * 24 * time.Hour)}, } api := &API{ Store: store, Hub: NewHub(store), Domain: "opentunnel.test", StartedAt: time.Now(), Issuer: issuer, IssuerName: "fake", } done := make(chan struct{}) go func() { api.runIssue(tunnel.ID, tunnel.Hostname, "CSR_A", "cert_a") close(done) }() <-issuer.started // The client binds a new key while the old order is still running. if err := store.bindCertificate(tunnel.ID, storedCert{ ID: "cert_b", CSR: "CSR_B", State: "ready", Issuer: "fake", Certificate: "CERT_B", Chain: "CHAIN_B", Expiry: expiryString(time.Now().Add(90 * 24 * time.Hour)), }); err != nil { t.Fatal(err) } close(issuer.release) <-done updated, _ := store.get(tunnel.ID) if updated.Certificate == nil || updated.Certificate.ID != "cert_b" || updated.Certificate.Certificate != "CERT_B" { t.Errorf("stale issuance overwrote the new bind: %+v", updated.Certificate) } }