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

import (
Max Adamo's avatar
Max Adamo committed
	"crypto/x509"
	"encoding/pem"
Max Adamo's avatar
Max Adamo committed
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
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"
)

var (
	appVersion             string
	buildTime              string
	CertBase               string
	KeyBase                string
	GroupName              string
	RedisBaseURL           string
	VaultBaseURL           string
	certificateDestination string
	fullchainDestination   string
	keyDestination         string
	caDestination          string
	Type                   string
)

Max Adamo's avatar
Max Adamo committed
// check certificates
func checkCerificates(dnsname string, certificate string, fullchain string, ca string, key string, days int, fail bool) bool {
	Seconds := days * 86400

	daysNumber := time.Now().Local().Add(time.Second * time.Duration(Seconds))

	//fmt.Printf(daysNumber)
	certPEM, err := ioutil.ReadFile(certificate)
	if err != nil {
		if fail == true {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] %v\n", err)
			os.Exit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	certFullchainPEM, err := ioutil.ReadFile(fullchain)
	if err != nil {
		if fail == true {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] %v\n", err)
			os.Exit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

	rootPEM, err := ioutil.ReadFile(ca)
	if err != nil {
		if fail == true {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] %v\n", err)
			os.Exit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}

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

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

	fullchainBlock, _ := pem.Decode([]byte(certFullchainPEM))
	if fullchainBlock == nil {
		if fail == true {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] failed to parse certificate PEM\n")
			os.Exit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}
	fullchainCert, fullchainErr := x509.ParseCertificate(fullchainBlock.Bytes)
	if fullchainErr != nil {
		if fail == true {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] failed to parse certificate %v\n", fullchainErr)
			os.Exit(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 {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] failed to parse certificate %v\n", err.Error())
			os.Exit(255)
Max Adamo's avatar
Max Adamo committed
		} else {
			return false
		}
	}
	if _, fullchainErr := fullchainCert.Verify(opts); fullchainErr != nil {
		if fail == true {
Max Adamo's avatar
Max Adamo committed
			fmt.Printf("[ERROR] failed to parse certificate %v\n", err.Error())
Max Adamo's avatar
Max Adamo committed
		} 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)
	req.SetBasicAuth("redis", redistoken)
	resp, err := client.Do(req)
	body, err := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()
	if err != nil {
Max Adamo's avatar
Max Adamo committed
		fmt.Printf("[ERROR] Fail to read %v: %v\n", redisurl, err)
		os.Exit(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)
	req.Header.Add("X-vault-token", vaulttoken)
	resp, err := vaultClient.Do(req)
	body, err := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()
	if err != nil {
		fmt.Printf("[ERROR] Fail to read %v: %v\n", vaulturl, err)
		os.Exit(255)
	}
	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, groupname string, filemode os.FileMode, dirmode os.FileMode) {
	baseDir := filepath.Dir(destination)
	if _, err := os.Stat(baseDir); os.IsNotExist(err) {
		os.MkdirAll(baseDir, 0755)
	}
	os.Chmod(baseDir, dirmode)

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

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

// 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" {
		CertBase = "/PATH/TO/CERTIFICATE"
		KeyBase = "/PATH/TO/PRIV/KEY"
		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 [--days=DAYS] [--type=TYPE] [--cert-destination=CERTDESTINATION] [--fullchain-destination=FULLCHAINDESTINATION] [--key-destination=KEYDESTINATION] [--ca-destination=CADESTINATION]
  acme-downloader -v | --version
  acme-downloader -b | --build
  acme-downloader -h | --help

Options:
  -h --help                                     Show this screen
  -v --version                                  Print version exit
  -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, dream_team, it, ne, ti...
  --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]
`, CertBase, CertBase, KeyBase, CertBase)

	// Annoyingly docopt tries to use 'version' the way he wants and I am using build

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

	if arguments["--build"] == true {
		fmt.Printf("acme-downloader version: %v, built on: %v\n", appVersion, buildTime)
		os.Exit(0)
	}

	VaultToken := arguments["--vault-token"].(string)
	CertName := arguments["--cert-name"].(string)
	CertNameUndercored := strings.Replace(CertName, ".", "_", -1)
	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 {
Max Adamo's avatar
Max Adamo committed
		fmt.Printf("Days mut be an integer\n")
		os.Exit(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"
	VaultURL := fmt.Sprintf("%v/%v/%v/vault_%v_key", VaultBaseURL, TeamName, CertName, CertNameUndercored)
	RedisCertURL := fmt.Sprintf("%v/%v:%v:redis_%v_pem.txt", RedisBaseURL, TeamName, CertName, CertNameUndercored)
	RedisCAURL := fmt.Sprintf("%v/%v:%v:redis_%v_chain_pem.txt", RedisBaseURL, TeamName, CertName, CertNameUndercored)
	RedisFullChainURL := fmt.Sprintf("%v/%v:%v:redis_%v_fullchain_pem.txt", RedisBaseURL, TeamName, CertName, CertNameUndercored)

Max Adamo's avatar
Max Adamo committed
	tmpCertificateDestination := "/tmp/amce_cert.pem"
	tmpFullchainDestination := "/tmp/amce_fullchain.pem"
	tmpCaDestination := "/tmp/amce_ca.pem"
	tmpKeyDestination := "/tmp/amce_key.pem"
Max Adamo's avatar
Max Adamo committed

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

	// check if there is a certificate installed and it is valid
	existingCert := checkCerificates(CertName, certificateDestination, fullchainDestination, caDestination, keyDestination, Days, false)
	if existingCert == true {
Max Adamo's avatar
Max Adamo committed
		fmt.Printf("the certificates are still valid\n")
Max Adamo's avatar
Max Adamo committed
		os.Exit(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
	WriteToFile(certificate, tmpCertificateDestination, GroupName, 0644, 0755)
	WriteToFile(fullChain, tmpFullchainDestination, GroupName, 0644, 0755)
	WriteToFile(ca, tmpCaDestination, GroupName, 0644, 0755)
	WriteToFile(privKey, tmpKeyDestination, GroupName, 0640, 0750)
Max Adamo's avatar
Max Adamo committed
	tempCertSlice := []string{tmpCertificateDestination, tmpFullchainDestination, tmpCaDestination, tmpKeyDestination}
Max Adamo's avatar
Max Adamo committed
	newCert := checkCerificates(CertName, tmpCertificateDestination, tmpFullchainDestination, tmpCaDestination, tmpKeyDestination, Days, false)
	if newCert == false {
Max Adamo's avatar
Max Adamo committed
		fmt.Printf("the certificates are malformed. Skippping installation\n")
		for _, element := range tempCertSlice {
			os.Remove(element)
		}
		os.Exit(255)
Max Adamo's avatar
Max Adamo committed
	}

Max Adamo's avatar
Max Adamo committed
	WriteToFile(certificate, certificateDestination, GroupName, 0644, 0755)
	WriteToFile(fullChain, fullchainDestination, GroupName, 0644, 0755)
	WriteToFile(ca, caDestination, GroupName, 0644, 0755)
	WriteToFile(privKey, keyDestination, GroupName, 0640, 0750)

	fmt.Printf("installed: %v\n", certificateDestination)
	fmt.Printf("installed: %v\n", caDestination)
	fmt.Printf("installed: %v\n", fullchainDestination)
	fmt.Printf("installed: %v\n", keyDestination)

}