-
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add certificate install and uninstall commands
`trellis certificate install` will install the root certificate from a Trellis development VM (by default) into your computer's system truststore. This means local development HTTPS sites will be considered secure by web browsers and won't show insecure warnings. `trellis certificate uninstall` removes the previously installed certificate from your computer's system "truststore".
- Loading branch information
1 parent
c63ec05
commit 7dd02a8
Showing
8 changed files
with
1,550 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package certificates | ||
|
||
import ( | ||
"crypto/tls" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/smallstep/certinfo" | ||
"github.com/smallstep/cli/crypto/pemutil" | ||
"github.com/smallstep/truststore" | ||
) | ||
|
||
func RootCertificatePath(configPath string) string { | ||
return filepath.Join(configPath, "root_certificates", "root_ca.crt") | ||
} | ||
|
||
func InstallFile(path string) error { | ||
opts := []truststore.Option{} | ||
opts = append(opts, truststore.WithFirefox(), truststore.WithJava()) | ||
|
||
if trustErr := truststore.InstallFile(path, opts...); trustErr != nil { | ||
switch err := trustErr.(type) { | ||
case *truststore.CmdError: | ||
return fmt.Errorf("failed to execute \"%s\" failed with: %v", strings.Join(err.Cmd().Args, " "), err.Error()) | ||
default: | ||
return fmt.Errorf("failed to install %s: %v", path, err.Error()) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func UninstallFile(path string) error { | ||
opts := []truststore.Option{} | ||
opts = append(opts, truststore.WithFirefox(), truststore.WithJava()) | ||
|
||
if trustErr := truststore.UninstallFile(path, opts...); trustErr != nil { | ||
switch err := trustErr.(type) { | ||
case *truststore.CmdError: | ||
return fmt.Errorf("failed to execute \"%s\" failed with: %v", strings.Join(err.Cmd().Args, " "), err.Error()) | ||
default: | ||
return fmt.Errorf("failed to uninstall %s: %v", path, err.Error()) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func ShortText(path string) (info string, err error) { | ||
if cert, err := pemutil.ReadCertificate(path); err == nil { | ||
if s, err := certinfo.CertificateShortText(cert); err == nil { | ||
return s, nil | ||
} | ||
} | ||
|
||
return "", fmt.Errorf("Error reading certificate: %v", err) | ||
} | ||
|
||
func FetchRootCertificate(path string, host string) (cert []byte, err error) { | ||
tr := &http.Transport{ | ||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | ||
} | ||
client := &http.Client{Transport: tr} | ||
|
||
res, err := client.Get(fmt.Sprintf("https://%s:8443/roots.pem", host)) | ||
if err != nil { | ||
return nil, fmt.Errorf("Could not fetch root certificate from server: %v", err) | ||
} | ||
|
||
body, err := io.ReadAll(res.Body) | ||
res.Body.Close() | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("Could not read response from server: %v", err) | ||
} | ||
|
||
if res.StatusCode != 200 { | ||
return nil, fmt.Errorf("Could not fetch root certificate from server: %d status code received", res.StatusCode) | ||
} | ||
|
||
return body, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package cmd | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/mitchellh/cli" | ||
"github.com/posener/complete" | ||
"github.com/roots/trellis-cli/certificates" | ||
"github.com/roots/trellis-cli/trellis" | ||
) | ||
|
||
type CertificateInstallCommand struct { | ||
UI cli.Ui | ||
Trellis *trellis.Trellis | ||
flags *flag.FlagSet | ||
path string | ||
} | ||
|
||
func NewCertificateInstallCommand(ui cli.Ui, trellis *trellis.Trellis) *CertificateInstallCommand { | ||
c := &CertificateInstallCommand{UI: ui, Trellis: trellis} | ||
c.init() | ||
return c | ||
} | ||
|
||
func (c *CertificateInstallCommand) init() { | ||
c.flags = flag.NewFlagSet("", flag.ContinueOnError) | ||
c.flags.Usage = func() { c.UI.Info(c.Help()) } | ||
c.flags.StringVar(&c.path, "path", "", "Local path to custom root certificate to install") | ||
} | ||
|
||
func (c *CertificateInstallCommand) Run(args []string) int { | ||
if err := c.Trellis.LoadProject(); err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
c.Trellis.CheckVirtualenv(c.UI) | ||
|
||
commandArgumentValidator := &CommandArgumentValidator{required: 0, optional: 1} | ||
commandArgumentErr := commandArgumentValidator.validate(args) | ||
if commandArgumentErr != nil { | ||
c.UI.Error(commandArgumentErr.Error()) | ||
c.UI.Output(c.Help()) | ||
return 1 | ||
} | ||
|
||
environment := "development" | ||
|
||
if len(args) == 1 { | ||
environment = args[0] | ||
environmentErr := c.Trellis.ValidateEnvironment(environment) | ||
if environmentErr != nil { | ||
c.UI.Error(environmentErr.Error()) | ||
return 1 | ||
} | ||
} | ||
|
||
if err := c.fetchRootCertificate(environment); err != nil { | ||
c.UI.Error("Error fetching root certificate from server:") | ||
c.UI.Error(err.Error()) | ||
c.UI.Error("The server (likely Vagrant virtual machine) must be running and have been provisioned with an SSL enabled site.") | ||
return 1 | ||
} | ||
|
||
if err := certificates.InstallFile(c.path); err != nil { | ||
c.UI.Error("Error installing root certificate to truststore:") | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
c.UI.Info(fmt.Sprintf("Certificate %s has been installed.\n", c.path)) | ||
if text, err := certificates.ShortText(c.path); err == nil { | ||
c.UI.Info(text) | ||
} | ||
|
||
c.UI.Info("Note: your web browser(s) will need to be restarted for this to take effect.") | ||
|
||
return 0 | ||
} | ||
|
||
func (c *CertificateInstallCommand) Synopsis() string { | ||
return "Installs a root certificate in the system truststore" | ||
} | ||
|
||
func (c *CertificateInstallCommand) Help() string { | ||
helpText := ` | ||
Usage: trellis certificate install [options] [ENVIRONMENT] | ||
Installs a root certificate in the system truststore. This allows your local | ||
computer to trust the "self-signed" root CA (certificate authority) that Trellis | ||
uses in development which avoids insecure warnings in your web browsers. | ||
By default this integrates with a Trellis server/VM and requires that it's running. | ||
However, the --path option can be used to specify any root certificate making this | ||
command useful for non-Trellis use cases too. | ||
Note: browsers may have to be restarted after running this command for it to take effect. | ||
Install a non-default root certificate via a local path: | ||
$ trellis certificate install --path ~/certs/root.crt | ||
Arguments: | ||
ENVIRONMENT Name of environment (default: development) | ||
Options: | ||
-h, --help show this help | ||
--path local path to custom root certificate to install | ||
` | ||
|
||
return strings.TrimSpace(helpText) | ||
} | ||
|
||
func (c *CertificateInstallCommand) AutocompleteArgs() complete.Predictor { | ||
return c.Trellis.AutocompleteEnvironment(c.flags) | ||
} | ||
|
||
func (c *CertificateInstallCommand) AutocompleteFlags() complete.Flags { | ||
return complete.Flags{ | ||
"--path": complete.PredictNothing, | ||
} | ||
} | ||
|
||
func (c *CertificateInstallCommand) fetchRootCertificate(environment string) error { | ||
if c.path == "" { | ||
c.path = certificates.RootCertificatePath(c.Trellis.ConfigPath()) | ||
} | ||
siteName, _ := c.Trellis.FindSiteNameFromEnvironment(environment, "") | ||
host := c.Trellis.SiteFromEnvironmentAndName(environment, siteName).MainHost() | ||
|
||
cert, err := certificates.FetchRootCertificate(c.path, host) | ||
|
||
if err = os.MkdirAll(filepath.Dir(c.path), os.ModePerm); err != nil { | ||
return err | ||
} | ||
|
||
if err = os.WriteFile(c.path, cert, os.ModePerm); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package cmd | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/mitchellh/cli" | ||
"github.com/roots/trellis-cli/trellis" | ||
) | ||
|
||
func TestCertificateInstallRunValidations(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
projectDetected bool | ||
args []string | ||
out string | ||
code int | ||
}{ | ||
{ | ||
"no_project", | ||
false, | ||
nil, | ||
"No Trellis project detected", | ||
1, | ||
}, | ||
{ | ||
"too_many_args", | ||
true, | ||
[]string{"foo", "bar"}, | ||
"Error: too many arguments", | ||
1, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
ui := cli.NewMockUi() | ||
trellis := trellis.NewMockTrellis(tc.projectDetected) | ||
galaxyInstallCommand := NewCertificateInstallCommand(ui, trellis) | ||
|
||
code := galaxyInstallCommand.Run(tc.args) | ||
|
||
if code != tc.code { | ||
t.Errorf("expected code %d to be %d", code, tc.code) | ||
} | ||
|
||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String() | ||
|
||
if !strings.Contains(combined, tc.out) { | ||
t.Errorf("expected output %q to contain %q", combined, tc.out) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package cmd | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
"github.com/mitchellh/cli" | ||
"github.com/posener/complete" | ||
"github.com/roots/trellis-cli/certificates" | ||
"github.com/roots/trellis-cli/trellis" | ||
) | ||
|
||
type CertificateUninstallCommand struct { | ||
UI cli.Ui | ||
Trellis *trellis.Trellis | ||
flags *flag.FlagSet | ||
path string | ||
} | ||
|
||
func NewCertificateUninstallCommand(ui cli.Ui, trellis *trellis.Trellis) *CertificateUninstallCommand { | ||
c := &CertificateUninstallCommand{UI: ui, Trellis: trellis} | ||
c.init() | ||
return c | ||
} | ||
|
||
func (c *CertificateUninstallCommand) init() { | ||
c.flags = flag.NewFlagSet("", flag.ContinueOnError) | ||
c.flags.Usage = func() { c.UI.Info(c.Help()) } | ||
c.flags.StringVar(&c.path, "path", "", "Local path to custom root certificate to uninstall") | ||
} | ||
|
||
func (c *CertificateUninstallCommand) Run(args []string) int { | ||
if err := c.Trellis.LoadProject(); err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
c.Trellis.CheckVirtualenv(c.UI) | ||
|
||
commandArgumentValidator := &CommandArgumentValidator{required: 0, optional: 0} | ||
commandArgumentErr := commandArgumentValidator.validate(args) | ||
if commandArgumentErr != nil { | ||
c.UI.Error(commandArgumentErr.Error()) | ||
c.UI.Output(c.Help()) | ||
return 1 | ||
} | ||
|
||
if c.path == "" { | ||
c.path = certificates.RootCertificatePath(c.Trellis.ConfigPath()) | ||
} | ||
|
||
if _, err := os.Stat(c.path); os.IsNotExist(err) { | ||
c.UI.Error(fmt.Sprintf("Root certificate not found: %s", c.path)) | ||
return 1 | ||
} | ||
|
||
if err := certificates.UninstallFile(c.path); err != nil { | ||
c.UI.Error("Error uninstalling root certificate to truststore:") | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
c.UI.Info(fmt.Sprintf("Certificate %s has been removed.\n", c.path)) | ||
c.UI.Info("Note: your web browser(s) will need to be restarted for this to take effect.") | ||
|
||
return 0 | ||
} | ||
|
||
func (c *CertificateUninstallCommand) Synopsis() string { | ||
return "Uninstalls a root certificate in the system truststore" | ||
} | ||
|
||
func (c *CertificateUninstallCommand) Help() string { | ||
helpText := ` | ||
Usage: trellis certificate uninstall [options] | ||
Uninstalls a root certificate in the system truststore. This will stop your computer/browser | ||
from trusting the root certificate authority. | ||
Note: browsers may have to be restarted after running this command for it to take effect. | ||
Uninstall a non-default root certificate via a local path: | ||
$ trellis certificate uninstall --path ~/certs/root.crt | ||
Options: | ||
-h, --help show this help | ||
--path local path to custom root certificate to uninstall | ||
` | ||
|
||
return strings.TrimSpace(helpText) | ||
} | ||
|
||
func (c *CertificateUninstallCommand) AutocompleteArgs() complete.Predictor { | ||
return c.Trellis.AutocompleteEnvironment(c.flags) | ||
} | ||
|
||
func (c *CertificateUninstallCommand) AutocompleteFlags() complete.Flags { | ||
return complete.Flags{ | ||
"--path": complete.PredictNothing, | ||
} | ||
} |
Oops, something went wrong.