Skip to content

Commit

Permalink
Merge pull request #147 from sol-eng/trust-ca
Browse files Browse the repository at this point in the history
Allow Administrators to optionally trust the CA in the provided SSL Certificate chain file
  • Loading branch information
samcofer authored Apr 30, 2023
2 parents a6190e4 + 446154e commit b364c15
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 21 deletions.
2 changes: 1 addition & 1 deletion cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func newSetup(setupOpts setupOpts) error {
return fmt.Errorf("issue selecting if SSL is to be used: %w", err)
}
if sslChoice {
certPath, keyPath, err := ssl.PromptAndVerifySSL()
certPath, keyPath, err := ssl.PromptAndVerifySSL(osType)
if err != nil {
return fmt.Errorf("issue verifying and configuring SSL: %w", err)
}
Expand Down
98 changes: 83 additions & 15 deletions internal/ssl/prompt.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package ssl

import (
"crypto/x509"
"errors"
"fmt"
"github.com/sol-eng/wbi/internal/config"
"strings"

"github.com/AlecAivazis/survey/v2"
log "github.com/sirupsen/logrus"
Expand All @@ -26,7 +27,7 @@ func PromptSSL() (bool, error) {
return name, nil
}

func PromptAndVerifySSL() (string, string, error) {
func PromptAndVerifySSL(osType config.OperatingSystem) (string, string, error) {
certPath, err := PromptSSLFilePath()
if err != nil {
return certPath, "", fmt.Errorf("issue with the provided SSL cert path: %w", err)
Expand All @@ -39,11 +40,30 @@ func PromptAndVerifySSL() (string, string, error) {
if err != nil {
return certPath, keyPath, fmt.Errorf("could not verify the SSL cert: %w", err)
}
serverCert, intermediateCertPool, _, err := ParseCertificateChain(certPath)

serverCert, intermediateCertPool, rootCert, err := ParseCertificateChain(certPath)
if err != nil {
return certPath, keyPath, fmt.Errorf("could not parse the certificate chain: %w", err)
}
var noRootOK bool
if rootCert == nil {
noRootOK, err = PromptRootCAMissing()
if err != nil {
return certPath, keyPath, fmt.Errorf("failure prompting for answer about RootCA missing: %w", err)
}
if !noRootOK {
return certPath, keyPath, fmt.Errorf("no root CA Certificate in the certificate chain,"+
" acquire the root and any necessary intermediate certificates and append them to the certificate file"+
" in this order from top to bottom, server -> intermediate -> root. To restart the installer from this"+
" step please use this command: 'wbi setup --step ssl': %w", err)
} else {
system.PrintAndLogInfo("Because your organization does not require intermediate and root" +
" certificates to be presented to your corporate browsers, you will need to work with your SSL team" +
" to get a copy of your organizations root and any intermediate certificates and add them to this" +
" Linux machines trust store. These certificates are required to facilitate inter-server communication" +
" between Workbench, Connect and Package Manager. An article on this topic can be found here:" +
"https://support.posit.co/hc/en-us/articles/4416056988567-RStudio-Team-SSL-Considerations")
}
}

certHostMisMatch, err := VerifySSLHostMatch(serverCert)

Expand All @@ -56,12 +76,34 @@ func PromptAndVerifySSL() (string, string, error) {
return certPath, keyPath, fmt.Errorf("hostname mismatch error, exit without proceeding: %w", err)
}
}

verified, err := VerifyTrustedCertificate(serverCert, intermediateCertPool)
if err != nil {
return certPath, keyPath, fmt.Errorf("failure while trying to verify server trust of the SSL cert: %w", err)
}
if verified {
system.PrintAndLogInfo("SSL successfully verified")
system.PrintAndLogInfo("SSL successfully verified")
} else {
trust, err := PromptAddRootCAToTrustStore()
if err != nil {
return certPath, keyPath, fmt.Errorf("failure while prompting administrator for "+
"certificate trust: %w", err)
}
if trust {
err = TrustRootCertificate(rootCert, osType)
if err != nil {
return certPath, keyPath, fmt.Errorf("failure while trying to trust the SSL cert: %w", err)
}
//re-verify certificate via Bash, because SystemCertPool isn't refreshed in this context
output, err := system.RunCommandAndCaptureOutput("openssl verify "+certPath, true, 1)
if err != nil {
return certPath, keyPath, fmt.Errorf("failure while trying to re-verify server trust of the SSL cert: %w", err)
}
if strings.Contains(output, "verification failed") {
return certPath, keyPath, fmt.Errorf("failure while trying to re-verify server trust of the SSL cert: %w", err)
}
system.PrintAndLogInfo("SSL certificate successfully trusted")
}
}

return certPath, keyPath, nil
Expand Down Expand Up @@ -101,29 +143,55 @@ func PromptSSLKeyFilePath() (string, error) {

func PromptMisMatchedHostName() (bool, error) {
name := false
messageText := "The hostname of your server and the subject name in the certificate " +
"don't match.\n This is common in configurations that include a load balancer " +
"or a proxy.\n If you would like to exit the installer, resolve the certificate mismatch\n" +
" and restart the installer at this step, you can run \"wbi setup --step ssl\" \n" +
"Please confirm that you want to proceed with mismatched names above?"
prompt := &survey.Confirm{
Message: "The hostname of your server and the subject name in the certificate " +
"don't match.\n This is common in configurations that include a load balancer " +
"or a proxy.\n If you would like to exit the installer, resolve the certificate mismatch\n" +
" and restart the installer at this step, you can run \"wbi setup --step ssl\" \n" +
"Please confirm that you want to proceed with mismatched names above?",
Message: messageText,
}
err := survey.AskOne(prompt, &name)
if err != nil {
return false, errors.New("there was an issue with the SSL prompt")
}
log.Info(messageText)
log.Info(name)
return name, nil
}

func PromptAddRootCAToTrustStore(*x509.Certificate) (bool, error) {
name := false
func PromptAddRootCAToTrustStore() (bool, error) {
name := true
messageText := "The certificate provided is not trusted by the system, this system level trust is usually required" +
"\n to support connectivity between systems. Would you like to add this untrusted root certificate " +
"\n to the system trust store?"
prompt := &survey.Confirm{
Message: "Would you like to add this untrusted root certificate to the system" +
"trust store?",
Message: messageText,
}
err := survey.AskOne(prompt, &name)
if err != nil {
return false, errors.New("there was an issue with the SSL prompt")
return false, errors.New("there was an issue with the CA Trust prompt")
}
log.Info(messageText)
log.Info(name)
return name, nil
}

func PromptRootCAMissing() (bool, error) {
name := true
messageText := "The certificate provided does not include a root Certificate Authority," +
" otherwise known as a Root CA Certificate. Generally, Posit products require a chain of certificates from server to" +
" your domains root certificate authority in order to present a valid HTTPS connection. In rare circumstances" +
" this is not required. Does your corporate browser trust the server certificate that you're using without the" +
" presence of a root certificate? If you are not sure, please select No."
prompt := &survey.Confirm{
Message: messageText,
}
err := survey.AskOne(prompt, &name)
if err != nil {
return false, errors.New("there was an issue with the missing Root Cert prompt")
}
log.Info(messageText)
log.Info(name)
return name, nil
}
36 changes: 36 additions & 0 deletions internal/ssl/trust.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ssl

import (
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/sol-eng/wbi/internal/config"
"github.com/sol-eng/wbi/internal/system"
)

func TrustRootCertificate(rootCA *x509.Certificate, osType config.OperatingSystem) error {
block := pem.Block{Type: "CERTIFICATE", Bytes: rootCA.Raw}
var pemCert []string
pemCert = append(pemCert, string(pem.EncodeToMemory(&block)))
switch osType {
case config.Ubuntu18, config.Ubuntu20, config.Ubuntu22:
err := system.WriteStrings(pemCert, "/usr/local/share/ca-certificates/workbenchCA.crt", 0755)
if err != nil {
return fmt.Errorf("writing certificate to disk failed: %w", err)
}
err = system.RunCommand("update-ca-certificates", true, 1)
if err != nil {
return fmt.Errorf("running command to trust root certificate: %w", err)
}
case config.Redhat7, config.Redhat8, config.Redhat9:
err := system.WriteStrings(pemCert, "/etc/pki/ca-trust/source/anchors/workbenchCA.crt", 0755)
if err != nil {
return fmt.Errorf("writing CA certificate to disk failed: %w", err)
}
err = system.RunCommand("update-ca-trust", true, 1)
if err != nil {
return fmt.Errorf("running command to trust root certificate: %w", err)
}
}
return nil
}
19 changes: 14 additions & 5 deletions internal/ssl/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func VerifyTrustedCertificate(serverCert *x509.Certificate, intermediateCA *x509
//Load system root CAs
rootCAs, err := x509.SystemCertPool()
if err != nil {
return true, fmt.Errorf("failed to load system root CAs: %w", err)
return false, fmt.Errorf("failed to load system root CAs: %w", err)
}

opts := x509.VerifyOptions{
Expand Down Expand Up @@ -111,14 +111,23 @@ func ParseCertificateChain(certLocation string) (*x509.Certificate, *x509.CertPo
}
if x509Cert.IsCA && x509Cert.Subject.CommonName != x509Cert.Issuer.CommonName {
intermediateCA.AddCert(x509Cert)
fmt.Println("This is an Intermediate Certificate:" + x509Cert.Subject.CommonName)
system.PrintAndLogInfo("This is an Intermediate Certificate:" + x509Cert.Subject.CommonName)
} else if !x509Cert.IsCA {
serverCert = x509Cert
fmt.Println("This is the Server Certificate:" + x509Cert.Subject.String())
} else {
system.PrintAndLogInfo("This is the Server Certificate:" + x509Cert.Subject.String())
} else if x509Cert.IsCA {
rootCert = x509Cert
fmt.Println("This is the Root Certificate:" + x509Cert.Subject.String())
system.PrintAndLogInfo("This is the Root Certificate:" + x509Cert.Subject.String())
}
}

if serverCert == nil {
return nil, nil, nil, fmt.Errorf("The server portion of your certificate is empty, or not in pem format")
}
if rootCert == nil {
system.PrintAndLogInfo("The root portion of your certificate is empty," +
" this is an odd, but not impossible configuration")
}

return serverCert, intermediateCA, rootCert, nil
}

0 comments on commit b364c15

Please sign in to comment.