235 lines
7.9 KiB
Go
235 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func openssl_path() (string, error) {
|
|
path, err := exec.LookPath("openssl")
|
|
if err != nil {
|
|
return "", fmt.Errorf("required command not found: openssl")
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func openssl_subject_name(commonName string) (string, error) {
|
|
if strings.ContainsAny(commonName, "\x00\r\n") {
|
|
return "", fmt.Errorf("certificate name cannot contain control characters")
|
|
}
|
|
escaped := strings.NewReplacer(`\`, `\\`, `/`, `\/`).Replace(commonName)
|
|
return "/CN=" + escaped, nil
|
|
}
|
|
|
|
func install_private_bytes(path string, contents []byte) error {
|
|
return write_private_file(path, func(writer io.Writer) error {
|
|
_, err := writer.Write(contents)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (app *App) generate_certificate() error {
|
|
openssl, err := openssl_path()
|
|
if err != nil {
|
|
return app.ui.message(err.Error())
|
|
}
|
|
if _, err := os.Stat(app.paths.CertFile); err == nil {
|
|
replace, err := app.ui.confirm("A certificate already exists. Replace it? This may affect your registered identity on servers.")
|
|
if err != nil || !replace {
|
|
return err
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return app.ui.message("Could not inspect the certificate: " + err.Error())
|
|
}
|
|
|
|
commonName, cancelled, err := app.ui.input("Enter a name for your certificate, such as your username:", "barnard", false)
|
|
if err != nil || cancelled {
|
|
return err
|
|
}
|
|
commonName = strings.TrimSpace(commonName)
|
|
if commonName == "" {
|
|
commonName = "barnard"
|
|
}
|
|
subject, err := openssl_subject_name(commonName)
|
|
if err != nil {
|
|
return app.ui.message(err.Error())
|
|
}
|
|
|
|
keyFile, err := os.CreateTemp(app.paths.ConfigDir, ".certificate-key-")
|
|
if err != nil {
|
|
return app.ui.message("Failed to create a certificate: " + err.Error())
|
|
}
|
|
keyPath := keyFile.Name()
|
|
keyFile.Close()
|
|
defer os.Remove(keyPath)
|
|
certificateFile, err := os.CreateTemp(app.paths.ConfigDir, ".certificate-public-")
|
|
if err != nil {
|
|
return app.ui.message("Failed to create a certificate: " + err.Error())
|
|
}
|
|
certificatePath := certificateFile.Name()
|
|
certificateFile.Close()
|
|
defer os.Remove(certificatePath)
|
|
|
|
command := exec.Command(openssl, "req", "-x509", "-newkey", "rsa:2048", "-keyout", keyPath, "-out", certificatePath, "-days", "3650", "-nodes", "-subj", subject)
|
|
if output, err := command.CombinedOutput(); err != nil {
|
|
app.log_line("OpenSSL certificate generation failed: " + strings.TrimSpace(string(output)))
|
|
return app.ui.message("Failed to generate certificate.")
|
|
}
|
|
privateKey, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return app.ui.message("Failed to read generated private key: " + err.Error())
|
|
}
|
|
certificate, err := os.ReadFile(certificatePath)
|
|
if err != nil {
|
|
return app.ui.message("Failed to read generated certificate: " + err.Error())
|
|
}
|
|
combined := append(append(append([]byte(nil), privateKey...), '\n'), certificate...)
|
|
if err := install_private_bytes(app.paths.CertFile, combined); err != nil {
|
|
return app.ui.message("Failed to install generated certificate: " + err.Error())
|
|
}
|
|
app.log_line("Generated certificate " + app.paths.CertFile)
|
|
return app.ui.message("Certificate generated successfully.")
|
|
}
|
|
|
|
func (app *App) view_certificate() error {
|
|
openssl, err := openssl_path()
|
|
if err != nil {
|
|
return app.ui.message(err.Error())
|
|
}
|
|
if _, err := os.Stat(app.paths.CertFile); os.IsNotExist(err) {
|
|
return app.ui.message("No certificate found: " + app.paths.CertFile)
|
|
} else if err != nil {
|
|
return app.ui.message("Could not inspect the certificate: " + err.Error())
|
|
}
|
|
command := exec.Command(openssl, "x509", "-in", app.paths.CertFile, "-noout", "-subject", "-dates", "-fingerprint")
|
|
output, err := command.CombinedOutput()
|
|
if err != nil || strings.TrimSpace(string(output)) == "" {
|
|
return app.ui.message("Could not read certificate information.")
|
|
}
|
|
return app.ui.message(strings.TrimSpace(string(output)))
|
|
}
|
|
|
|
func validate_certificate_pair(openssl, path string) error {
|
|
certificateCheck := exec.Command(openssl, "x509", "-in", path, "-noout")
|
|
if err := certificateCheck.Run(); err != nil {
|
|
return fmt.Errorf("the file does not contain a valid PEM certificate")
|
|
}
|
|
keyCheck := exec.Command(openssl, "pkey", "-in", path, "-check", "-noout")
|
|
if err := keyCheck.Run(); err != nil {
|
|
return fmt.Errorf("the file does not contain a valid private key")
|
|
}
|
|
certificatePublic, err := exec.Command(openssl, "x509", "-in", path, "-pubkey", "-noout").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("could not read the certificate public key")
|
|
}
|
|
keyPublic, err := exec.Command(openssl, "pkey", "-in", path, "-pubout").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("could not read the private key public key")
|
|
}
|
|
if !bytes.Equal(bytes.TrimSpace(certificatePublic), bytes.TrimSpace(keyPublic)) {
|
|
return fmt.Errorf("the certificate and private key do not match")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (app *App) import_certificate() error {
|
|
openssl, err := openssl_path()
|
|
if err != nil {
|
|
return app.ui.message(err.Error())
|
|
}
|
|
rawPath, cancelled, err := app.ui.input("Enter the full path to a PEM file containing both the certificate and private key:", "", false)
|
|
if err != nil || cancelled {
|
|
return err
|
|
}
|
|
rawPath = strings.TrimSpace(rawPath)
|
|
if rawPath == "" {
|
|
return nil
|
|
}
|
|
path, err := expand_user_path(rawPath)
|
|
if err != nil {
|
|
return app.ui.message("Could not resolve certificate path: " + err.Error())
|
|
}
|
|
info, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
return app.ui.message("File not found: " + path)
|
|
}
|
|
if err != nil {
|
|
return app.ui.message("Could not inspect certificate file: " + err.Error())
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return app.ui.message("Certificate path is not a regular file: " + path)
|
|
}
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return app.ui.message("Failed to read certificate: " + err.Error())
|
|
}
|
|
validationFile, err := os.CreateTemp(app.paths.ConfigDir, ".certificate-import-")
|
|
if err != nil {
|
|
return app.ui.message("Failed to prepare certificate validation: " + err.Error())
|
|
}
|
|
validationPath := validationFile.Name()
|
|
defer os.Remove(validationPath)
|
|
if err := validationFile.Chmod(0600); err != nil {
|
|
validationFile.Close()
|
|
return app.ui.message("Failed to protect certificate validation file: " + err.Error())
|
|
}
|
|
if _, err := validationFile.Write(contents); err != nil {
|
|
validationFile.Close()
|
|
return app.ui.message("Failed to prepare certificate validation: " + err.Error())
|
|
}
|
|
if err := validationFile.Close(); err != nil {
|
|
return app.ui.message("Failed to prepare certificate validation: " + err.Error())
|
|
}
|
|
if err := validate_certificate_pair(openssl, validationPath); err != nil {
|
|
return app.ui.message(err.Error() + ".")
|
|
}
|
|
if _, err := os.Stat(app.paths.CertFile); err == nil {
|
|
samePath := false
|
|
sourcePath, sourceErr := filepath.EvalSymlinks(path)
|
|
destinationPath, destinationErr := filepath.EvalSymlinks(app.paths.CertFile)
|
|
if sourceErr == nil && destinationErr == nil {
|
|
samePath = sourcePath == destinationPath
|
|
}
|
|
if samePath {
|
|
return app.ui.message("That certificate is already the active Barnard certificate.")
|
|
}
|
|
replace, err := app.ui.confirm("A certificate already exists. Replace it?")
|
|
if err != nil || !replace {
|
|
return err
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return app.ui.message("Could not inspect the existing certificate: " + err.Error())
|
|
}
|
|
if err := install_private_bytes(app.paths.CertFile, contents); err != nil {
|
|
return app.ui.message("Failed to import certificate: " + err.Error())
|
|
}
|
|
app.log_line("Imported certificate " + app.paths.CertFile)
|
|
return app.ui.message("Certificate imported successfully.")
|
|
}
|
|
|
|
func (app *App) manage_certificate() error {
|
|
options := []string{"Generate", "View", "Import", "Go Back"}
|
|
for {
|
|
selection, cancelled, err := app.ui.menu(options)
|
|
if err != nil || cancelled || selection == len(options)-1 {
|
|
return err
|
|
}
|
|
switch options[selection] {
|
|
case "Generate":
|
|
err = app.generate_certificate()
|
|
case "View":
|
|
err = app.view_certificate()
|
|
case "Import":
|
|
err = app.import_certificate()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|