diff --git a/src/jetstream/api/structs.go b/src/jetstream/api/structs.go index 61f20bd75b..4eda7e0a10 100644 --- a/src/jetstream/api/structs.go +++ b/src/jetstream/api/structs.go @@ -373,6 +373,7 @@ type PortalConfig struct { TLSCertKey string `configName:"CONSOLE_PROXY_CERT_KEY"` TLSCertPath string `configName:"CONSOLE_PROXY_CERT_PATH"` TLSCertKeyPath string `configName:"CONSOLE_PROXY_CERT_KEY_PATH"` + TLSCertGenerate bool `configName:"CONSOLE_PROXY_CERT_GENERATE"` CFClient string `configName:"CF_CLIENT"` CFClientSecret string `configName:"CF_CLIENT_SECRET"` AllowedOrigins []string `configName:"ALLOWED_ORIGINS"` diff --git a/src/jetstream/main.go b/src/jetstream/main.go index ac5c839f61..5d4809d229 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1,14 +1,22 @@ package main import ( + "bytes" + "crypto/rand" + "crypto/rsa" "crypto/sha1" "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "database/sql" "encoding/gob" "encoding/hex" + "encoding/pem" "errors" "fmt" "io" + "io/ioutil" + "math/big" "net" "net/http" "os" @@ -655,7 +663,15 @@ func detectTLSCert(pc api.PortalConfig) (string, string, error) { return pc.TLSCertPath, pc.TLSCertKeyPath, nil } - err := os.WriteFile(certFilename, []byte(pc.TLSCert), 0600) + // Check if we should generate self-signed certificates + if pc.TLSCertGenerate { + log.Info("Generating TLS certificates") + if err := generateTLSCert(&pc); err != nil { + return "", "", err + } + } + + err := ioutil.WriteFile(certFilename, []byte(pc.TLSCert), 0600) if err != nil { return "", "", err } @@ -667,6 +683,37 @@ func detectTLSCert(pc api.PortalConfig) (string, string, error) { return certFilename, certKeyFilename, nil } +// generateTLSCert generates a self-signed TLS certificate for localhost. The +// resulting certificate is stored in the PortalConfig as TLSCert and TLSCertKey. +func generateTLSCert(pc *api.PortalConfig) error { + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return fmt.Errorf("could not generate TLS certificate private key: %w", err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{CommonName: "localhost"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 100), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.ParseIP("::1")}, + DNSNames: []string{"localhost"}, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return fmt.Errorf("could not generate TLS certificate: %w", err) + } + out := &bytes.Buffer{} + pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + pc.TLSCert = out.String() + out.Reset() + pem.Encode(out, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + pc.TLSCertKey = out.String() + return nil +} + func newPortalProxy(pc api.PortalConfig, dcp *sql.DB, ss HttpSessionStore, sessionStoreOptions *sessions.Options, env *env.VarSet) *portalProxy { log.Debug("newPortalProxy") diff --git a/src/jetstream/main_test.go b/src/jetstream/main_test.go index 2c9d8d6f1a..edb6d40774 100644 --- a/src/jetstream/main_test.go +++ b/src/jetstream/main_test.go @@ -1,12 +1,18 @@ package main import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/pem" "testing" "github.com/govau/cf-common/env" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" "github.com/cloudfoundry-incubator/stratos/src/jetstream/api" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" ) // DO NOT DELETE - this is necessary for thr HTTP Client used during unit tests @@ -155,3 +161,49 @@ func TestLoadDatabaseConfigWithInvalidSSLMode(t *testing.T) { t.Errorf("Unexpected success - should not be able to load database configs with an invalid SSL Mode specified.") } } + +func TestGenerateTLSCert(t *testing.T) { + pc := api.PortalConfig{} + err := generateTLSCert(&pc) + if err != nil { + t.Errorf("Error generating TLS certificate: %v", err) + return + } + certBlock, rest := pem.Decode([]byte(pc.TLSCert)) + if len(rest) > 0 { + t.Errorf("Extra bytes after certificate: %s", rest) + } + if certBlock.Type != "CERTIFICATE" { + t.Errorf("Invalid cerificate block type %s", certBlock.Type) + } + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + t.Errorf("Error parsing certificate: %v", err) + return + } + if err := cert.VerifyHostname("localhost"); err != nil { + t.Errorf("Certificate does not work for localhost: %v", err) + } + privBlock, rest := pem.Decode([]byte(pc.TLSCertKey)) + if len(rest) > 0 { + t.Errorf("Extra bytes after private key: %s", rest) + } + if privBlock.Type != "RSA PRIVATE KEY" { + t.Errorf("Invalid private key block type %s", privBlock.Type) + } + privKey, err := x509.ParsePKCS1PrivateKey(privBlock.Bytes) + if err != nil { + t.Logf("Could not parse private key: %s", err) + t.FailNow() + } + data := []byte("sample data") + hash := sha512.Sum512(data) + sig, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA512, hash[:]) + if err != nil { + t.Logf("Could not sign data: %s", err) + t.FailNow() + } + if err = cert.CheckSignature(x509.SHA512WithRSA, data, sig); err != nil { + t.Errorf("Certifcate public key does not match private key") + } +}