Skip to content
Snippets Groups Projects
main.go 14.7 KiB
Newer Older
Max Adamo's avatar
Max Adamo committed
package main

import (
	"crypto/tls"
Max Adamo's avatar
Max Adamo committed
	"crypto/x509"
	"encoding/pem"
Max Adamo's avatar
Max Adamo committed
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"os/user"
Max Adamo's avatar
Max Adamo committed
	"path/filepath"
Max Adamo's avatar
Max Adamo committed
	"runtime"
Max Adamo's avatar
Max Adamo committed
	"strconv"
Max Adamo's avatar
Max Adamo committed
	"strings"
Max Adamo's avatar
Max Adamo committed
	"time"
Max Adamo's avatar
Max Adamo committed

	"github.com/docopt/docopt-go"
	"github.com/go-ini/ini"
	"github.com/tidwall/gjson"
)

const errMsg string = "[ERR]"
const infoMsg string = "[INFO]"

Max Adamo's avatar
Max Adamo committed
var (
Max Adamo's avatar
Max Adamo committed
	appVersion                string
	buildTime                 string
	CertBase                  string
	KeyBase                   string
	GroupName                 string
	GroupID                   int
Max Adamo's avatar
Max Adamo committed
	RedisBaseURL              string
	VaultBaseURL              string
Massimiliano Adamo's avatar
Massimiliano Adamo committed
	VaultURL                  string
	RedisCertURL              string
	RedisCAURL                string
	RedisFullChainURL         string
Max Adamo's avatar
Max Adamo committed
	certificateDestination    string
	fullchainDestination      string
	keyDestination            string
	caDestination             string
	Type                      string
	tmpCertificateDestination string
	tmpFullchainDestination   string
	tmpCaDestination          string
	tmpKeyDestination         string
	certTmpDir                string
	key                       *x509.Certificate
	cert                      *x509.Certificate
Max Adamo's avatar
Max Adamo committed
)

Max Adamo's avatar
Max Adamo committed
// app clean and exit
Max Adamo's avatar
Max Adamo committed
func appExit(status int) {
Max Adamo's avatar
Max Adamo committed
	if runtime.GOOS == "windows" {
		certTmpDir = "C:\\tmp\\acme-downloader\\"
	} else {
		certTmpDir = "/tmp/acme-downloader"
	}
	err := os.RemoveAll(certTmpDir)
Max Adamo's avatar
Max Adamo committed
	if err != nil {
		fmt.Printf("%v could not delete temporary directory: %v\n", errMsg, err)
Max Adamo's avatar
Max Adamo committed
	}
	os.Exit(status)
}

Max Adamo's avatar
Max Adamo committed
// check certificates
Max Adamo's avatar
Max Adamo committed
func checkCertificates(dnsname string, certificate string, fullchain string, ca string, key string, days int, fail bool) bool {
Max Adamo's avatar
Max Adamo committed

Max Adamo's avatar
Max Adamo committed
	Seconds := days * 86400
Max Adamo's avatar
Max Adamo committed
	daysNumber := time.Now().Local().Add(time.Second * time.Duration(Seconds))

	certPEM, err := ioutil.ReadFile(certificate)
	if err != nil {
		if fail == true {
Massimiliano Adamo's avatar
Massimiliano Adamo committed
			fmt.Printf("%v could not access certificate %v\n", errMsg, err)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	certFullchainPEM, err := ioutil.ReadFile(fullchain)
	if err != nil {
		if fail == true {
Massimiliano Adamo's avatar
Massimiliano Adamo committed
			fmt.Printf("%v could not access full chain %v\n", errMsg, err)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	rootPEM, err := ioutil.ReadFile(ca)
	if err != nil {
		if fail == true {
Massimiliano Adamo's avatar
Massimiliano Adamo committed
			fmt.Printf("%v could not access full CA %v\n", errMsg, err)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	roots := x509.NewCertPool()
	ok := roots.AppendCertsFromPEM([]byte(rootPEM))
	if !ok {
		if fail == true {
			fmt.Printf("%v failed to parse root certificate\n", errMsg)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	block, _ := pem.Decode([]byte(certPEM))
	if block == nil {
		if fail == true {
			fmt.Printf("%v failed to parse certificate PEM\n", errMsg)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}
Max Adamo's avatar
Max Adamo committed
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		if fail == true {
			fmt.Printf("%v failed to parse certificate %v\n", errMsg, err)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	fullchainBlock, _ := pem.Decode([]byte(certFullchainPEM))
	if fullchainBlock == nil {
		if fail == true {
			fmt.Printf("%v failed to parse certificate PEM\n", errMsg)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}
	fullchainCert, fullchainErr := x509.ParseCertificate(fullchainBlock.Bytes)
	if fullchainErr != nil {
		if fail == true {
			fmt.Printf("%v failed to parse certificate %v\n", errMsg, fullchainErr)
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	opts := x509.VerifyOptions{
		Roots:         roots,
		DNSName:       dnsname,
		CurrentTime:   daysNumber,
		Intermediates: x509.NewCertPool(),
	}

	if _, err := cert.Verify(opts); err != nil {
		if fail == true {
			fmt.Printf("%v failed to parse certificate %v\n", errMsg, err.Error())
Max Adamo's avatar
Max Adamo committed
			appExit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}
	if _, fullchainErr := fullchainCert.Verify(opts); fullchainErr != nil {
		if fail == true {
			fmt.Printf("%v failed to parse certificate %v\n", errMsg, err.Error())
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}
	return true
}

// check if the private key matches the publick key
func checkPrivkey(privkey string, pubkey string, fail bool) bool {
	_, errFileExist := os.Stat(privkey)
	if os.IsNotExist(errFileExist) {
		fmt.Printf("%v could not access the private key %v\n", errMsg, privkey)
		appExit(255)
	}
	_, err := tls.LoadX509KeyPair(pubkey, privkey)
	if fail == true {
		if err != nil {
			fmt.Printf("%v the private key %v does not match the the public certificate %v\n", errMsg, privkey, pubkey)
			appExit(255)
		} else {
			return false
		}
	}
	return true
}

Max Adamo's avatar
Max Adamo committed
// get redis key
func GetRedisKey(redisurl string, redistoken string) string {
	client := &http.Client{}
	req, err := http.NewRequest("GET", redisurl, nil)
Max Adamo's avatar
Max Adamo committed
	if err != nil {
		fmt.Printf("%v Fail to read %v: %v\n", errMsg, redisurl, err)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
	}
Max Adamo's avatar
Max Adamo committed
	req.SetBasicAuth("redis", redistoken)
	resp, err := client.Do(req)
	body, err := ioutil.ReadAll(resp.Body)
Max Adamo's avatar
Max Adamo committed
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		fmt.Printf("%v Fail to fetch %v\n", errMsg, redisurl)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
	}
Max Adamo's avatar
Max Adamo committed
	defer resp.Body.Close()
	if err != nil {
		fmt.Printf("%v Fail to read %v: %v\n", errMsg, redisurl, err)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
Max Adamo's avatar
Max Adamo committed
	}
	return fmt.Sprintf(string(body))
}

Max Adamo's avatar
Max Adamo committed
// get Vault key
func GetVaultKey(vaulturl string, vaulttoken string) string {
	vaultClient := &http.Client{}
	req, err := http.NewRequest("GET", vaulturl, nil)
Max Adamo's avatar
Max Adamo committed
	if err != nil {
		fmt.Printf("%v Fail to read %v: %v\n", errMsg, vaulturl, err)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
	}
Max Adamo's avatar
Max Adamo committed
	req.Header.Add("X-vault-token", vaulttoken)
	resp, err := vaultClient.Do(req)
	body, err := ioutil.ReadAll(resp.Body)
Max Adamo's avatar
Max Adamo committed
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		fmt.Printf("%v Fail to fetch %v\n", errMsg, vaulturl)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
	}
Max Adamo's avatar
Max Adamo committed
	defer resp.Body.Close()
	if err != nil {
		fmt.Printf("%v Fail to read %v: %v\n", errMsg, vaulturl, err)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
Max Adamo's avatar
Max Adamo committed
	}
	return gjson.Get(string(body), "data.value").String()
}

Max Adamo's avatar
Max Adamo committed
// create directory structure and write certificate to file
func WriteToFile(content string, destination string, filemode os.FileMode) {
Max Adamo's avatar
Max Adamo committed
	baseDir := filepath.Dir(destination)
	if _, err := os.Stat(baseDir); os.IsNotExist(err) {
		os.MkdirAll(baseDir, 0755)
	}

	file, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE, filemode)
	if err != nil {
		fmt.Printf("%v %v cannot be created\n", errMsg, destination)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
Max Adamo's avatar
Max Adamo committed
	}

	fmt.Fprintf(file, "%v\n", content)
	file.Close()
}

// move temp file to destination
func moveFile(source string, destination string, groupid int, filemode os.FileMode, dirmode os.FileMode) {
	baseDir := filepath.Dir(destination)
	if _, err := os.Stat(baseDir); os.IsNotExist(err) {
		os.MkdirAll(baseDir, 0755)
	}
	err := os.Rename(source, destination)
	if err != nil {
		fmt.Printf("%v Fail to install %v: %v\n", errMsg, destination, err)
		appExit(255)
	}
	if runtime.GOOS != "windows" {
		err = os.Chown(destination, 0, groupid)
		if err != nil {
			fmt.Printf("%v Changing file owner to %v", errMsg, groupid)
	fmt.Printf("%v installed: %v\n", infoMsg, destination)
Max Adamo's avatar
Max Adamo committed
// ReadOSRelease from /etc/os-release
func ReadOSRelease(configfile string) map[string]string {
Max Adamo's avatar
Max Adamo committed
	ConfigParams := make(map[string]string)
Max Adamo's avatar
Max Adamo committed
	cfg, err := ini.Load(configfile)
	if err != nil {
Max Adamo's avatar
Max Adamo committed
		ConfigParams["ID"] = "unknown"
	} else {
		ConfigParams["ID"] = cfg.Section("").Key("ID").String()
Max Adamo's avatar
Max Adamo committed
	}

	return ConfigParams
}

func main() {

	OSInfo := ReadOSRelease("/etc/os-release")
	OSRelease := OSInfo["ID"]
	if OSRelease == "centos" || OSRelease == "rhel" {
		CertBase = "/etc/pki/tls/certs"
		KeyBase = "/etc/pki/tls/private"
		GroupName = "root"
	} else if OSRelease == "ubuntu" || OSRelease == "debian" {
		CertBase = "/etc/ssl/certs"
		KeyBase = "/etc/ssl/private"
		GroupName = "ssl-cert"
Max Adamo's avatar
Max Adamo committed
	} else if OSRelease == "arch" {
		CertBase = "/etc/ssl/certs"
		KeyBase = "/etc/ssl/private"
		GroupName = "root"
Max Adamo's avatar
Max Adamo committed
	} else if OSRelease == "unknown" {
Max Adamo's avatar
Max Adamo committed
		if runtime.GOOS == "windows" {
			CertBase = "C:\\ACME\\certificates"
			KeyBase = "C:\\ACME\\private"
Max Adamo's avatar
Max Adamo committed
			GroupName = "root"
		} else {
			CertBase = "/etc/acme/certs"
			KeyBase = "/etc/acme/private"
Max Adamo's avatar
Max Adamo committed
			GroupName = "root"
		}
Max Adamo's avatar
Max Adamo committed
	}

	usage := fmt.Sprintf(`ACME Downloader:
  - fetches and stores a given Certificate, Full Chain, CA and Private Key

Usage:
  acme-downloader --redis-token=REDISTOKEN --vault-token=VAULTTOKEN --cert-name=CERTNAME --team-name=TEAMNAME [--silent] [--days=DAYS] [--type=TYPE] [--cert-destination=CERTDESTINATION] [--fullchain-destination=FULLCHAINDESTINATION] [--key-destination=KEYDESTINATION] [--ca-destination=CADESTINATION] [--wildcard]
Massimiliano Adamo's avatar
Massimiliano Adamo committed
  acme-downloader -h | --help
Max Adamo's avatar
Max Adamo committed
  acme-downloader -v | --version
  acme-downloader -b | --build

Options:
  -h --help                                     Show this screen
  -v --version                                  Print version information and exit
Max Adamo's avatar
Max Adamo committed
  -b --build                                    Print version and build information and exit
  --redis-token=REDISTOKEN                      Redis access token
  --vault-token=VAULTTOKEN                      Vault access token
  --cert-name=CERTNAME                          Certificate name
  --team-name=TEAMNAME                          Team name: swd, it, ne, ti...
Max Adamo's avatar
Max Adamo committed
  --days=DAYS                                   Days before expiration [default: 30]
  --type=TYPE                                   Type, EV or OV [default: EV]
  --cert-destination=CERTDESTINATION            Cert Destination [default: %v/<cert-name>.crt]
  --fullchain-destination=FULLCHAINDESTINATION  Full Chain Destination[default: %v/<cert-name>_fullchain.crt]
  --key-destination=KEYDESTINATION              Key Destination [default: %v/<cert-name>.key]
  --ca-destination=CADESTINATION                CA Destination [default: %v/COMODO_<type>.crt]
Massimiliano Adamo's avatar
Massimiliano Adamo committed
  --wildcard                                    The certificate type is wildcard
Max Adamo's avatar
Max Adamo committed
`, CertBase, CertBase, KeyBase, CertBase)

	arguments, _ := docopt.Parse(usage, nil, true, appVersion, false)

	if arguments["--build"] == true {
		fmt.Printf("acme-downloader version: %v, built on: %v\n", appVersion, buildTime)
Max Adamo's avatar
Max Adamo committed
		appExit(0)
Max Adamo's avatar
Max Adamo committed
	}

Max Adamo's avatar
Max Adamo committed
	if runtime.GOOS == "windows" {
		tmpCertificateDestination = "C:\\tmp\\acme-downloader\\cert\\amce_cert.pem"
		tmpFullchainDestination = "C:\\tmp\\acme-downloader\\cert\\amce_fullchain.pem"
		tmpCaDestination = "C:\\tmp\\acme-downloader\\cert\\amce_ca.pem"
		tmpKeyDestination = "C:\\tmp\\acme-downloader\\key\\amce_key.pem"
		GroupID = 0 // just a fake one
Max Adamo's avatar
Max Adamo committed
	} else {
		tmpCertificateDestination = "/tmp/acme-downloader/cert/amce_cert.pem"
		tmpFullchainDestination = "/tmp/acme-downloader/cert/amce_fullchain.pem"
		tmpCaDestination = "/tmp/acme-downloader/cert/amce_ca.pem"
		tmpKeyDestination = "/tmp/acme-downloader/key/amce_key.pem"
		group, groupErr := user.LookupGroup(GroupName)
		if groupErr != nil {
			fmt.Printf("%v Fail looking up %v user user info\n", errMsg, GroupName)
			appExit(255)
		}
		GroupID, _ = strconv.Atoi(group.Gid)
Max Adamo's avatar
Max Adamo committed
	VaultToken := arguments["--vault-token"].(string)
	CertName := arguments["--cert-name"].(string)
Massimiliano Adamo's avatar
Massimiliano Adamo committed
	CertNameUnderscored := strings.Replace(CertName, ".", "_", -1)
Max Adamo's avatar
Max Adamo committed
	TeamName := arguments["--team-name"].(string)
	RedisToken := arguments["--redis-token"].(string)
	Type = arguments["--type"].(string)
Max Adamo's avatar
Max Adamo committed
	DayString := arguments["--days"].(string)
	Days, daysErr := strconv.Atoi(DayString)
	if daysErr != nil {
		fmt.Printf("%v Days mut be an integer\n", errMsg)
Max Adamo's avatar
Max Adamo committed
		appExit(255)
Max Adamo's avatar
Max Adamo committed
	}
Max Adamo's avatar
Max Adamo committed
	RedisBaseURL = "https://redis.geant.org/GET"
	VaultBaseURL = "https://vault.geant.org/v1"
Massimiliano Adamo's avatar
Massimiliano Adamo committed
	if arguments["--wildcard"] == true {
		VaultURL = fmt.Sprintf("%v/%v/common/vault_sectigo_ov_wildcard_%v_key", VaultBaseURL, TeamName, CertNameUnderscored)
		RedisCertURL = fmt.Sprintf("%v/%v:common:redis_sectigo_ov_%v_pem.txt", RedisBaseURL, TeamName, CertNameUnderscored)
		RedisCAURL = fmt.Sprintf("%v/%v:common:redis_sectigo_ov_%v_chain_pem.txt", RedisBaseURL, TeamName, CertNameUnderscored)
		RedisFullChainURL = fmt.Sprintf("%v/%v:common:redis_sectigo_ov_%v_fullchain_pem.txt", RedisBaseURL, TeamName, CertNameUnderscored)
	} else {
		VaultURL = fmt.Sprintf("%v/%v/%v/vault_%v_key", VaultBaseURL, TeamName, CertName, CertNameUnderscored)
		RedisCertURL = fmt.Sprintf("%v/%v:%v:redis_%v_pem.txt", RedisBaseURL, TeamName, CertName, CertNameUnderscored)
		RedisCAURL = fmt.Sprintf("%v/%v:%v:redis_%v_chain_pem.txt", RedisBaseURL, TeamName, CertName, CertNameUnderscored)
		RedisFullChainURL = fmt.Sprintf("%v/%v:%v:redis_%v_fullchain_pem.txt", RedisBaseURL, TeamName, CertName, CertNameUnderscored)
	}
Max Adamo's avatar
Max Adamo committed

Max Adamo's avatar
Max Adamo committed
	if arguments["--cert-destination"] == fmt.Sprintf(filepath.Join(CertBase, "<cert-name>.crt")) {
		certificateDestination = fmt.Sprintf(filepath.Join(CertBase, fmt.Sprintf("%v.crt", CertName)))
Max Adamo's avatar
Max Adamo committed
	} else {
		certificateDestination = arguments["--cert-destination"].(string)
	}
Max Adamo's avatar
Max Adamo committed
	if arguments["--fullchain-destination"] == fmt.Sprintf(filepath.Join(CertBase, "<cert-name>_fullchain.crt")) {
		fullchainDestination = fmt.Sprintf(filepath.Join(CertBase, fmt.Sprintf("%v_fullchain.crt", CertName)))
Max Adamo's avatar
Max Adamo committed
	} else {
		fullchainDestination = arguments["--fullchain-destination"].(string)
	}
Max Adamo's avatar
Max Adamo committed
	if arguments["--ca-destination"] == fmt.Sprintf(filepath.Join(CertBase, "COMODO_<type>.crt")) {
		caDestination = fmt.Sprintf(filepath.Join(CertBase, fmt.Sprintf("COMODO_%v.crt", Type)))
Max Adamo's avatar
Max Adamo committed
	} else {
		caDestination = arguments["--ca-destination"].(string)
	}
Max Adamo's avatar
Max Adamo committed
	if arguments["--key-destination"] == fmt.Sprintf(filepath.Join(KeyBase, "<cert-name>.key")) {
		keyDestination = fmt.Sprintf(filepath.Join(KeyBase, fmt.Sprintf("%v.key", CertName)))
Max Adamo's avatar
Max Adamo committed
	} else {
		keyDestination = arguments["--key-destination"].(string)
	}
Max Adamo's avatar
Max Adamo committed

	// check if there is a certificate installed and it is valid
Max Adamo's avatar
Max Adamo committed
	existingCert := checkCertificates(CertName, certificateDestination, fullchainDestination, caDestination, keyDestination, Days, false)
	existingKey := checkPrivkey(keyDestination, certificateDestination, false)
Max Adamo's avatar
Max Adamo committed
	if existingCert == true && existingKey == true {
		fmt.Printf("%v the certificate is still valid\n", infoMsg)
Max Adamo's avatar
Max Adamo committed
		appExit(0)
Max Adamo's avatar
Max Adamo committed
	}
Max Adamo's avatar
Max Adamo committed
	certificate := GetRedisKey(RedisCertURL, RedisToken)
	ca := GetRedisKey(RedisCAURL, RedisToken)
	fullChain := GetRedisKey(RedisFullChainURL, RedisToken)
Max Adamo's avatar
Max Adamo committed
	privKey := GetVaultKey(VaultURL, VaultToken)
Max Adamo's avatar
Max Adamo committed

Max Adamo's avatar
Max Adamo committed
	// download and test certificates on a temporary location
	WriteToFile(certificate, tmpCertificateDestination, 0644)
	WriteToFile(fullChain, tmpFullchainDestination, 0644)
	WriteToFile(ca, tmpCaDestination, 0644)
	WriteToFile(privKey, tmpKeyDestination, 0640)
Max Adamo's avatar
Max Adamo committed

Max Adamo's avatar
Max Adamo committed
	checkCertificates(CertName, tmpCertificateDestination, tmpFullchainDestination, tmpCaDestination, tmpKeyDestination, Days, true)
	checkPrivkey(tmpKeyDestination, tmpCertificateDestination, true)
Max Adamo's avatar
Max Adamo committed

	// move certificates in place
	moveFile(tmpCertificateDestination, certificateDestination, GroupID, 0644, 0755)
	moveFile(tmpFullchainDestination, fullchainDestination, GroupID, 0644, 0755)
	moveFile(tmpCaDestination, caDestination, GroupID, 0644, 0755)
	moveFile(tmpKeyDestination, keyDestination, GroupID, 0640, 0750)
Max Adamo's avatar
Max Adamo committed

	// Exit 64: it means that the certificate was replaced and the
	// application can be reloaded to make use of the new certificate
Max Adamo's avatar
Max Adamo committed

Max Adamo's avatar
Max Adamo committed
}