Files

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
}
}
}