feat: configure HTTP/HTTPS public entry and managed certificates
This commit is contained in:
1 parent
35596cda9d
commit
88ea6b7854
21 files changed
+1309
-149
No files matched your search
@@ -0,0 +1,155 @@
|
||||
package tlsservice
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testPEMPair(t *testing.T, dnsNames []string) (certPEM, keyPEM string, otherKeyPEM string) {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{CommonName: dnsNames[0], Organization: []string{"opsx-vpn-test"}},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: dnsNames,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certPEMBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyDER, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
otherDER, err := x509.MarshalECPrivateKey(other)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(certPEMBytes),
|
||||
string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})),
|
||||
string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: otherDER}))
|
||||
}
|
||||
|
||||
func TestInstallUploadedCertificateSuccess(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_DOMAIN", "")
|
||||
t.Setenv("VPN_TLS_SCHEME", "")
|
||||
t.Setenv("VPN_TLS_PORT", "")
|
||||
t.Setenv("VPN_TLS_MODE", "")
|
||||
dir := t.TempDir()
|
||||
certPEM, keyPEM, _ := testPEMPair(t, []string{"vpn.example.com"})
|
||||
if err := ConfigureMode(dir, "nginx"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ConfigurePublicEntry(dir, "https", "vpn.example.com", 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := InstallUploadedCertificate(dir, "vpn.example.com", certPEM, keyPEM); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fullchain, privkey := UploadedCertificatePaths(dir, "vpn.example.com")
|
||||
if st, err := os.Stat(fullchain); err != nil || st.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("fullchain perms: %v %v", st, err)
|
||||
}
|
||||
if st, err := os.Stat(privkey); err != nil || st.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("privkey perms: %v %v", st, err)
|
||||
}
|
||||
if got := readCertSource(dir); got != CertSourceUpload {
|
||||
t.Fatalf("cert_source=%q", got)
|
||||
}
|
||||
t.Setenv("VPN_TLS_HTTP_ADDR", "127.0.0.1:0")
|
||||
t.Setenv("VPN_TLS_HTTPS_ADDR", "127.0.0.1:0")
|
||||
if err := Start(http.NotFoundHandler(), dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status := Status()
|
||||
if !status.Valid || status.CertSource != CertSourceUpload || status.Domain != "vpn.example.com" {
|
||||
t.Fatalf("status after upload: %+v", status)
|
||||
}
|
||||
if status.Issuer == "" || status.NotAfter.IsZero() {
|
||||
t.Fatalf("missing issuer/expiry: %+v", status)
|
||||
}
|
||||
if status.PublicBase != "https://vpn.example.com" {
|
||||
t.Fatalf("public_base=%q", status.PublicBase)
|
||||
}
|
||||
fc, pk, source := CertificateFiles("vpn.example.com")
|
||||
if source != CertSourceUpload || fc != fullchain || pk != privkey {
|
||||
t.Fatalf("CertificateFiles = %q %q %q", fc, pk, source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallUploadedCertificateKeyMismatch(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPEM, _, otherKey := testPEMPair(t, []string{"vpn.example.com"})
|
||||
err := InstallUploadedCertificate(dir, "vpn.example.com", certPEM, otherKey)
|
||||
if err == nil {
|
||||
t.Fatal("expected mismatch error")
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(dir, "tls", "certs", "vpn.example.com", "fullchain.pem")); !os.IsNotExist(statErr) {
|
||||
t.Fatal("mismatch must not write certificate files")
|
||||
}
|
||||
if readCertSource(dir) != "" {
|
||||
t.Fatal("mismatch must not mark cert_source")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallUploadedCertificateDomainMismatch(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPEM, keyPEM, _ := testPEMPair(t, []string{"other.example.com"})
|
||||
if err := InstallUploadedCertificate(dir, "vpn.example.com", certPEM, keyPEM); err == nil {
|
||||
t.Fatal("expected hostname mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadRollbackPreservesPreviousCertificate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
firstCert, firstKey, _ := testPEMPair(t, []string{"vpn.example.com"})
|
||||
secondCert, secondKey, _ := testPEMPair(t, []string{"vpn.example.com"})
|
||||
if err := InstallUploadedCertificate(dir, "vpn.example.com", firstCert, firstKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fullchain, privkey := UploadedCertificatePaths(dir, "vpn.example.com")
|
||||
if err := InstallUploadedCertificateWithApply(dir, "vpn.example.com", secondCert, secondKey, func() error {
|
||||
return errors.New("nginx reload failed")
|
||||
}); err == nil {
|
||||
t.Fatal("expected reload error")
|
||||
}
|
||||
gotCert, err := os.ReadFile(fullchain)
|
||||
if err != nil || strings.TrimSpace(string(gotCert)) != strings.TrimSpace(firstCert) {
|
||||
t.Fatalf("old certificate not restored: %v", err)
|
||||
}
|
||||
gotKey, err := os.ReadFile(privkey)
|
||||
if err != nil || strings.TrimSpace(string(gotKey)) != strings.TrimSpace(firstKey) {
|
||||
t.Fatalf("old key not restored: %v", err)
|
||||
}
|
||||
if readCertSource(dir) != CertSourceUpload {
|
||||
t.Fatal("certificate source not restored")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user