-
Massimiliano Adamo authoredMassimiliano Adamo authored
main.go 18.06 KiB
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/docopt/docopt-go"
"github.com/go-ini/ini"
"github.com/hashicorp/go-version"
"github.com/tidwall/gjson"
)
const errMsg string = "[ERR]"
const infoMsg string = "[INFO]"
type Properties struct {
Version []string `json:"version"`
}
type JsonArtifact struct {
Properties Properties
Uri string
}
var (
appVersion string
buildTime string
CertBase string
KeyBase string
GroupName string
GroupID int
RedisBaseURL string
VaultBaseURL string
VaultURL string
RedisCertURL string
RedisCAURL string
RedisFullChainURL string
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
httpClient = &http.Client{Timeout: 10 * time.Second}
)
// app clean and exit
func appExit(status int) {
if runtime.GOOS == "windows" {
certTmpDir = "C:\\tmp\\acme-downloader\\"
} else {
certTmpDir = "/tmp/acme-downloader"
}
err := os.RemoveAll(certTmpDir)
if err != nil {
fmt.Printf("%v could not delete temporary directory: %v\n", errMsg, err)
}
os.Exit(status)
}
// get upstream version
func getUpstreamVersion(url string) string {
resp, err := httpClient.Get(url)
if err != nil {
fmt.Printf("Request to Artifactory failed: %v\n", err)
appExit(255)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Reading body from Artifactory failed: %v\n", err)
appExit(255)
}
return string(body)
}
// check certificates
func checkCertificates(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))
certPEM, err := ioutil.ReadFile(certificate)
if err != nil {
if fail == true {
fmt.Printf("%v could not access certificate %v\n", errMsg, err)
appExit(255)
} else {
return false
}
}
certFullchainPEM, err := ioutil.ReadFile(fullchain)
if err != nil {
if fail == true {
fmt.Printf("%v could not access full chain %v\n", errMsg, err)
appExit(255)
} else {
return false
}
}
rootPEM, err := ioutil.ReadFile(ca)
if err != nil {
if fail == true {
fmt.Printf("%v could not access full CA %v\n", errMsg, err)
appExit(255)
} 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)
appExit(255)
} else {
return false
}
}
block, _ := pem.Decode([]byte(certPEM))
if block == nil {
if fail == true {
fmt.Printf("%v failed to parse certificate PEM\n", errMsg)
appExit(255)
} else {
return false
}
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
if fail == true {
fmt.Printf("%v failed to parse certificate %v\n", errMsg, err)
appExit(255)
} else {
return false
}
}
fullchainBlock, _ := pem.Decode([]byte(certFullchainPEM))
if fullchainBlock == nil {
if fail == true {
fmt.Printf("%v failed to parse full chain PEM\n", errMsg)
appExit(255)
} else {
return false
}
}
fullchainCert, fullchainErr := x509.ParseCertificate(fullchainBlock.Bytes)
if fullchainErr != nil {
if fail == true {
fmt.Printf("%v failed to parse full chain %v\n", errMsg, fullchainErr)
appExit(255)
} 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())
appExit(255)
} else {
return false
}
}
if _, fullchainErr := fullchainCert.Verify(opts); fullchainErr != nil {
if fail == true {
fmt.Printf("%v failed to parse full chain %v\n", errMsg, err.Error())
} 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
}
// get redis key
func GetRedisKey(redisurl string, redistoken string) string {
client := &http.Client{}
req, err := http.NewRequest("GET", redisurl, nil)
if err != nil {
fmt.Printf("%v Fail to read %v: %v\n", errMsg, redisurl, err)
appExit(255)
}
req.SetBasicAuth("redis", redistoken)
resp, err := client.Do(req)
body, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode > 299 {
fmt.Printf("%v Fail to fetch %v\n", errMsg, redisurl)
appExit(255)
}
defer resp.Body.Close()
if err != nil {
fmt.Printf("%v Fail to read %v: %v\n", errMsg, redisurl, err)
appExit(255)
}
return fmt.Sprintf(string(body))
}
// get Vault key
func GetVaultKey(vaulturl string, vaulttoken string) string {
vaultClient := &http.Client{}
req, err := http.NewRequest("GET", vaulturl, nil)
if err != nil {
fmt.Printf("%v Fail to read %v: %v\n", errMsg, vaulturl, err)
appExit(255)
}
req.Header.Add("X-vault-token", vaulttoken)
resp, err := vaultClient.Do(req)
body, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode > 299 {
fmt.Printf("%v Fail to fetch %v\n", errMsg, vaulturl)
appExit(255)
}
defer resp.Body.Close()
if err != nil {
fmt.Printf("%v Fail to read %v: %v\n", errMsg, vaulturl, err)
appExit(255)
}
return gjson.Get(string(body), "data.value").String()
}
// create directory structure and write certificate to file
func WriteToFile(content string, destination string, filemode os.FileMode) {
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)
appExit(255)
}
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)
appExit(255)
}
}
fmt.Printf("%v installed: %v\n", infoMsg, destination)
}
// ReadOSRelease from /etc/os-release
func ReadOSRelease(configfile string) map[string]string {
ConfigParams := make(map[string]string)
cfg, err := ini.Load(configfile)
if err != nil {
ConfigParams["ID"] = "unknown"
} else {
ConfigParams["ID"] = cfg.Section("").Key("ID").String()
}
return ConfigParams
}
// download file: taken as is from StackOverflow
func downloadFile(filepath string, url string) (err error) {
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
// Writer the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
// change filemode
err = os.Chmod(filepath, 0755)
if err != nil {
return err
}
return nil
}
func main() {
progName := filepath.Base(os.Args[0])
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"
} else if OSRelease == "arch" {
CertBase = "/etc/ssl/certs"
KeyBase = "/etc/ssl/private"
GroupName = "root"
} else if OSRelease == "unknown" {
if runtime.GOOS == "windows" {
CertBase = "C:\\ACME\\certificates"
KeyBase = "C:\\ACME\\private"
GroupName = "root"
} else {
CertBase = "/etc/acme/certs"
KeyBase = "/etc/acme/private"
GroupName = "root"
}
}
usage := fmt.Sprintf(`ACME Downloader:
- fetches and stores a given Certificate, Full Chain, CA and Private Key
Usage:
%v --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]
%v -h | --help
%v -v | --version
%v -b | --build
%v --update
%v --check-version
Options:
-h --help Show this screen
-v --version Print version information and 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, 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]
--wildcard The certificate type is wildcard
--update Self-updates the tool and exit
--check-version Check upstream version
`, progName, progName, progName, progName, progName, progName, CertBase, CertBase, KeyBase, CertBase)
arguments, _ := docopt.Parse(usage, nil, true, appVersion, false)
if arguments["--build"] == true {
fmt.Printf("%v version: %v, built on: %v\n", progName, appVersion, buildTime)
appExit(0)
}
// chekc upstream version
metadataBase := "https://artifactory.software.geant.org/artifactory/api/storage/acme-downloader/acme-downloader"
metadataURL := fmt.Sprintf("%v_%v_%v?properties=version", metadataBase, runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
metadataURL = fmt.Sprintf("%v_%v_%v.exe?properties=version", metadataBase, runtime.GOOS, runtime.GOARCH)
}
upstreamResp := getUpstreamVersion(metadataURL)
var jsonartifact JsonArtifact
json.Unmarshal([]byte(upstreamResp), &jsonartifact)
upstreamVersion := fmt.Sprintf(jsonartifact.Properties.Version[0])
localVersion, _ := version.NewVersion(appVersion)
remoteVersion, _ := version.NewVersion(upstreamVersion)
if localVersion.GreaterThan(remoteVersion) {
fmt.Printf("local version (%s) is older than upstream version (%s)\n", localVersion, remoteVersion)
fmt.Printf("please use --update to install the new version\n")
} else {
if arguments["--check-version"] == true {
fmt.Printf("You are running the latest version (%v)\n", appVersion)
}
}
if arguments["--check-version"] == true {
appExit(0)
}
// fetch and install the upstream version
if arguments["--update"] == true {
ArtifactoryBase := "https://artifactory.software.geant.org/artifactory/acme-downloader"
ArtifactName := fmt.Sprintf("acme-downloader_%v_%v", runtime.GOOS, runtime.GOARCH)
ToolName := os.Args[0]
ArtifactURL := fmt.Sprintf("%v/%v", ArtifactoryBase, ArtifactName)
if runtime.GOOS == "windows" {
ArtifactURL = fmt.Sprintf("%v/%v.exe", ArtifactoryBase, ArtifactName)
}
deleteErr := os.Remove(ToolName)
if deleteErr != nil {
fmt.Printf("Error deleting %v: %v!\n", ToolName, deleteErr)
appExit(2)
}
downloadErr := downloadFile(ToolName, ArtifactURL)
if downloadErr != nil {
fmt.Printf("Error downloading %v: %v!\n", ToolName, downloadErr)
appExit(2)
}
fmt.Printf("%v updated successfully!\n", ToolName)
appExit(0)
}
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
} 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)
}
VaultToken := arguments["--vault-token"].(string)
CertName := arguments["--cert-name"].(string)
CertNameUnderscored := strings.Replace(CertName, ".", "_", -1)
TeamName := arguments["--team-name"].(string)
RedisToken := arguments["--redis-token"].(string)
Type = arguments["--type"].(string)
DayString := arguments["--days"].(string)
Days, daysErr := strconv.Atoi(DayString)
if daysErr != nil {
fmt.Printf("%v Days mut be an integer\n", errMsg)
appExit(255)
}
RedisBaseURL = "https://redis.geant.org/GET"
VaultBaseURL = "https://vault.geant.org/v1"
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)
}
if arguments["--cert-destination"] == fmt.Sprintf(filepath.Join(CertBase, "<cert-name>.crt")) {
certificateDestination = fmt.Sprintf(filepath.Join(CertBase, fmt.Sprintf("%v.crt", CertName)))
} else {
certificateDestination = arguments["--cert-destination"].(string)
}
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)))
} else {
fullchainDestination = arguments["--fullchain-destination"].(string)
}
if arguments["--ca-destination"] == fmt.Sprintf(filepath.Join(CertBase, "COMODO_<type>.crt")) {
caDestination = fmt.Sprintf(filepath.Join(CertBase, fmt.Sprintf("COMODO_%v.crt", Type)))
} else {
caDestination = arguments["--ca-destination"].(string)
}
if arguments["--key-destination"] == fmt.Sprintf(filepath.Join(KeyBase, "<cert-name>.key")) {
keyDestination = fmt.Sprintf(filepath.Join(KeyBase, fmt.Sprintf("%v.key", CertName)))
} else {
keyDestination = arguments["--key-destination"].(string)
}
// check if there is a certificate installed and it is valid
existingCert := checkCertificates(CertName, certificateDestination, fullchainDestination, caDestination, keyDestination, Days, false)
existingKey := checkPrivkey(keyDestination, certificateDestination, false)
if existingCert == true && existingKey == true {
fmt.Printf("%v the certificate is still valid\n", infoMsg)
appExit(0)
}
certificate := GetRedisKey(RedisCertURL, RedisToken)
ca := GetRedisKey(RedisCAURL, RedisToken)
fullChain := GetRedisKey(RedisFullChainURL, RedisToken)
privKey := GetVaultKey(VaultURL, VaultToken)
// download and test certificates on a temporary location
WriteToFile(certificate, tmpCertificateDestination, 0644)
WriteToFile(fullChain, tmpFullchainDestination, 0644)
WriteToFile(ca, tmpCaDestination, 0644)
WriteToFile(privKey, tmpKeyDestination, 0640)
checkCertificates(CertName, tmpCertificateDestination, tmpFullchainDestination, tmpCaDestination, tmpKeyDestination, Days, true)
checkPrivkey(tmpKeyDestination, tmpCertificateDestination, true)
// 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)
// Exit 64: it means that the certificate was replaced and the
// application can be reloaded to make use of the new certificate
appExit(64)
}