Initial commit

master
eater 5 years ago
commit 080c1a3d06
Signed by: eater
GPG Key ID: 656785D50BE51C0A

3
.gitignore vendored

@ -0,0 +1,3 @@
out
examples/*
!examples/service.toml

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2019 eater
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,40 @@
package service
import (
"github.com/BurntSushi/toml"
"log"
"os"
"path/filepath"
)
type ZeroooConfig struct {
Location string `toml:"location"`
Endpoint string `toml:"endpoint"`
}
type Config struct {
Zerooo ZeroooConfig `toml:"zerooo"`
}
func LoadConfig(path string) (*Config, error) {
config := Config{}
_, err := toml.DecodeFile(path, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func CreateConfig(path string, cfg *Config) {
conf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
Check(err, "Couldn't open %q, err: %s", path, err)
encoder := toml.NewEncoder(conf)
log.Printf("ZeroooConfig: %#v", cfg)
err = encoder.Encode(cfg)
if cfg.Zerooo.Location[0] != '/' {
cfg.Zerooo.Location = filepath.Join(filepath.Dir(path), cfg.Zerooo.Location)
}
Check(err, "Couldn't parse %q, err: %s", path, err)
}

@ -0,0 +1,3 @@
[zerooo]
location = "/etc/zerooo"
endpoint = "https://zer.ooo"

@ -0,0 +1,217 @@
package service
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
)
import "golang.org/x/crypto/blowfish"
type Status int
const (
OK Status = iota
WAITING
ERROR
)
type BaseMessage struct {
Signature string
}
type httpServer struct {
manager *Manager
status Status
}
type CreateCSRRequest struct {
*BaseMessage
Hostname string
}
type CreateCSRResponse struct {
*BaseMessage
CSR string `json:"csr"`
}
type UpdateOpenVPNConfigRequest struct {
*BaseMessage
Config string
}
type DeliverCertificateRequest struct {
*BaseMessage
Certificate string
}
func NewHttpServer(manager *Manager) *httpServer {
return &httpServer{manager, WAITING}
}
func (it *httpServer) Start() {
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(200)
writer.Write([]byte("Don't"))
})
http.HandleFunc("/create-csr", func(writer http.ResponseWriter, request *http.Request) {
req := &CreateCSRRequest{}
err := it.verifyRequest(request, req)
if err != nil {
writer.WriteHeader(400)
return
}
csr, err := it.manager.CreateCSR(req.Hostname)
if err != nil {
writer.WriteHeader(500)
return
}
it.writeResponse(writer, CreateCSRResponse{
CSR: string(csr),
})
})
http.HandleFunc("/deliver-crt", func(writer http.ResponseWriter, request *http.Request) {
req := &DeliverCertificateRequest{}
err := it.verifyRequest(request, req)
if err != nil {
writer.WriteHeader(400)
return
}
err = it.manager.UpdateCertificate(req.Certificate)
if err != nil {
writer.WriteHeader(500)
return
}
it.manager.openVPN.Start()
})
http.HandleFunc("/update-openvpn-config", func(writer http.ResponseWriter, request *http.Request) {
req := &UpdateOpenVPNConfigRequest{}
err := it.verifyRequest(request, req)
if err != nil {
writer.WriteHeader(400)
return
}
err = it.manager.openVPN.UpdateConfig(req.Config)
if err != nil {
writer.WriteHeader(500)
return
}
it.manager.openVPN.Restart()
})
http.ListenAndServe(":7864", nil)
}
func (it *httpServer) writeResponse(writer http.ResponseWriter, v interface{}) error {
passwordAndIV := make([]byte, 48)
_, err := rand.Read(passwordAndIV)
if err != nil {
return err
}
encPasswordAndIV, err := rsa.EncryptPKCS1v15(rand.Reader, it.manager.CAPublicKey(), passwordAndIV)
if err != nil {
return err
}
_, err = writer.Write([]byte(hex.EncodeToString(encPasswordAndIV)))
if err != nil {
return err
}
password := passwordAndIV[:32]
iv := passwordAndIV[32:]
cipher, err := blowfish.NewSaltedCipher(password, iv)
if err != nil {
return err
}
hashedFingerpint := sha256.Sum256(it.manager.GetServerFingerprint())
plainSignature, err := rsa.SignPKCS1v15(rand.Reader, it.manager.privateKey, 0, hashedFingerpint[:])
if err != nil {
return err
}
sign, ok := v.(*BaseMessage)
if !ok {
return errors.New("Given message can't be signed")
}
sign.Signature = hex.EncodeToString(plainSignature)
body, err := json.Marshal(v)
if err != nil {
return err
}
encBody := make([]byte, len(body))
cipher.Encrypt(encBody, body)
writer.Write([]byte(hex.EncodeToString(encBody)))
return nil
}
func (it *httpServer) verifyRequest(r *http.Request, v interface{}) (error) {
all, err := ioutil.ReadAll(hex.NewDecoder(r.Body))
if err != nil {
return err
}
encPasswordAndIV := all[:256]
encBody := all[256:]
passwordAndIV, err := rsa.DecryptPKCS1v15(rand.Reader, it.manager.privateKey, encPasswordAndIV)
if err != nil {
return err
}
password := passwordAndIV[:32]
iv := passwordAndIV[32:]
cipher, err := blowfish.NewSaltedCipher(password, iv)
if err != nil {
return err
}
body := make([]byte, len(encBody))
cipher.Decrypt(body, encBody)
err = json.Unmarshal(body, v)
if err != nil {
return err
}
baseReq, ok := v.(*BaseMessage)
if !ok {
return errors.New("non-base request")
}
signature, err := hex.DecodeString(baseReq.Signature)
if err != nil {
return err
}
fingerprint := it.manager.GetServerFingerprint()
fingerprintHashed := sha256.Sum256(fingerprint)
err = rsa.VerifyPKCS1v15(it.manager.CAPublicKey(), 0, fingerprintHashed[:], signature)
return err
}

@ -0,0 +1,339 @@
package service
import (
"bufio"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
)
type Manager struct {
Config Config
privateKey *rsa.PrivateKey
openVPN *OpenVPN
CA *x509.Certificate
HasCertificate bool
HasOpenVPNConfig bool
}
func NewManager(cfg Config) *Manager {
return &Manager{
Config: cfg,
}
}
func fileExists(s string) bool {
if _, err := os.Stat(s); err == nil {
return true
} else if os.IsNotExist(err) {
return false
} else {
Check(err, "Failed to check %q, err: %s", s, err)
}
return false
}
func (it *Manager) GetOpenVPNConfigLocation() string {
return it.Config.Zerooo.Location + "/server.conf"
}
func (it *Manager) GetCALocation() string {
return it.Config.Zerooo.Location + "/ca.crt"
}
func (it *Manager) GetCertificateLocation() string {
return it.Config.Zerooo.Location + "/server.crt"
}
func (it *Manager) GetKeyLocation() string {
return it.Config.Zerooo.Location + "/server.key"
}
func (it *Manager) GetDHLocation() string {
return it.Config.Zerooo.Location + "/dh2048.pem"
}
func (it *Manager) GetCRLLocation() string {
return it.Config.Zerooo.Location + "/crl.pem"
}
func (it *Manager) EnsureFile(path string, generator func()) bool {
if !fileExists(path) {
generator()
return true
}
return false
}
func (it *Manager) Init() {
if !fileExists(it.Config.Zerooo.Location) {
os.MkdirAll(it.Config.Zerooo.Location, 0755)
}
generated := it.EnsureFile(it.GetKeyLocation(), func() {
it.GenerateKey()
})
if !generated {
it.LoadKey()
}
it.EnsureFile(it.GetCALocation(), func() {
it.DownloadCA()
})
it.LoadCA()
it.EnsureFile(it.GetCRLLocation(), func() {
it.DownloadCRL()
})
it.EnsureFile(it.GetDHLocation(), func() {
it.DownloadDH()
})
}
func (it *Manager) GetServerFingerprint() []byte {
der := it.ExportPublicKeyDER()
fp := it.GetOpenSSLLikeFingerprint(der)
return []byte(fp)
}
func (it *Manager) GetOpenSSLLikeFingerprint(input []byte) string {
output := md5.Sum(input)
items := make([]string, 16)
for i := range output {
items[i] = strings.ToLower(hex.EncodeToString([]byte{output[i]}))
}
return strings.Join(items, ":")
}
func (it *Manager) Register() {
fp := it.GetServerFingerprint()
log.Printf("%s\n", fp)
signed, err := rsa.SignPKCS1v15(rand.Reader, it.privateKey, 0, fp)
Check(err, "Failed signing fingerprint, err: %s", err)
signedHash := sha256.Sum256(signed)
publicKey := string(it.ExportPublicKey())
hexSignature := hex.EncodeToString(signedHash[:])
log.Printf("Registering with\n\nsignature: %s\npublic key:\n\n%s", hexSignature, publicKey)
resp, err := http.PostForm(it.Config.Zerooo.Endpoint+"/server/register", url.Values{
"publicKey": []string{publicKey},
"signature": []string{hexSignature},
})
Check(err, "Failed to sent register post to endpoint (%s/server/register), err: %s", it.Config.Zerooo.Endpoint, err)
if resp.StatusCode != 200 {
Error("Endpoint responded with %d, may not be actual zerooo endpoint", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
Check(err, "Failed to read response, err: %s", err)
strBody := string(body)
items := strings.SplitN(strBody, "\n", 2)
code, err := strconv.ParseInt(items[0], 10, 64)
Check(err, "Failed reading response success code, err: %s", err)
if code != 0 {
Error("Register failed, server responded with:\n\n%s", items[1])
}
log.Printf("Register succesful, server responded with: %s", items[1])
}
func (it *Manager) ExportPublicKeyDER() []byte {
if (it.privateKey == nil) {
Error("No private key is loaded, can't export public key")
}
der, err := x509.MarshalPKIXPublicKey(&it.privateKey.PublicKey)
Check(err, "Failed converting public key to DER, err: %s", err)
return der
}
func (it *Manager) ExportPublicKey() []byte {
pemKey := &pem.Block{
Type: "PUBLIC KEY",
Bytes: it.ExportPublicKeyDER(),
}
return pem.EncodeToMemory(pemKey)
}
func (it *Manager) GenerateKey() {
log.Printf("No key exists, generating new 4096 RSA key")
priv, err := rsa.GenerateKey(rand.Reader, 4096)
Check(err, "Couldn't generate key, err: %s", err)
it.privateKey = priv
pemKey := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
}
keyFile, err := os.OpenFile(it.GetKeyLocation(), os.O_EXCL|os.O_CREATE|os.O_RDWR, 0600)
Check(err, "Failed creating file at %q for private key, err: %s", it.GetKeyLocation(), err)
defer keyFile.Close()
err = pem.Encode(keyFile, pemKey)
Check(err, "Failed encoding private key, err: %s", err)
}
func (it *Manager) LoadKey() {
log.Printf("Loading generated key")
keyFile, err := os.Open(it.GetKeyLocation())
Check(err, "Failed opening private key at %q for reading, err: %s", it.GetKeyLocation(), err)
defer keyFile.Close()
stat, _ := keyFile.Stat()
var size int64 = stat.Size()
pembytes := make([]byte, size)
buffer := bufio.NewReader(keyFile)
_, err = buffer.Read(pembytes)
Check(err, "Failed to read private key at %q, err: %s", it.GetKeyLocation(), err)
data, _ := pem.Decode([]byte(pembytes))
if data == nil {
Error("No valid key found at %q", it.GetKeyLocation())
}
importedKey, err := x509.ParsePKCS1PrivateKey(data.Bytes)
Check(err, "Failed importing private key at %q, err: %s", it.GetKeyLocation(), err)
it.privateKey = importedKey
}
func (it *Manager) Daemon() {
server := NewHttpServer(it)
go server.Start()
it.openVPN = NewOpenVPN(it)
it.openVPN.Start()
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT)
for sig := range ch {
log.Printf("Received signal %s, quitting.", sig)
os.Exit(0)
}
}
func (it *Manager) LoadCA() {
log.Printf("Loading CA certificate")
keyFile, err := os.Open(it.GetCALocation())
Check(err, "Failed opening CA certificate at %q for reading, err: %s", it.GetKeyLocation(), err)
defer keyFile.Close()
stat, _ := keyFile.Stat()
var size int64 = stat.Size()
pemBytes := make([]byte, size)
buffer := bufio.NewReader(keyFile)
_, err = buffer.Read(pemBytes)
Check(err, "Failed to read CA certificate at %q, err: %s", it.GetKeyLocation(), err)
data, _ := pem.Decode([]byte(pemBytes))
if data == nil {
Error("No valid CA certificate found at %q", it.GetKeyLocation())
}
ca, err := x509.ParseCertificate(data.Bytes)
Check(err, "Failed importing private key at %q, err: %s", it.GetKeyLocation(), err)
it.CA = ca
}
func (it *Manager) DownloadCA() {
caUrl := it.Config.Zerooo.Endpoint + "/ca"
download("CA", it.GetCALocation(), caUrl)
}
func (it *Manager) CAPublicKey() (*rsa.PublicKey) {
key, ok := it.CA.PublicKey.(*rsa.PublicKey)
if !ok {
Error("Can't read CA Certificate as public key")
}
return key
}
func (it *Manager) CreateCSR(hostname string) ([]byte, error) {
tpl := x509.CertificateRequest{Subject: pkix.Name{CommonName: hostname}}
csr, err := x509.CreateCertificateRequest(rand.Reader, &tpl, it.privateKey)
if err != nil {
return nil, err
}
pemCSR := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr,
}
return pem.EncodeToMemory(pemCSR), nil
}
func (it *Manager) UpdateCertificate(s string) error {
f, err := os.OpenFile(it.GetCertificateLocation(), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(s)
return err
}
func download(name string, path string, url string) {
resp, err := http.Get(url)
Check(err, "Failed fetching %s at %q, err: %s", name, url, err)
cert, err := ioutil.ReadAll(resp.Body)
Check(err, "Failed fetching %s at %q, err: %s", name, url, err)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
Check(err, "Failed open %s for writing at %q, err: %s", name, path, err)
defer f.Close()
_, err = f.Write(cert)
Check(err, "Failed writing %s at %q, err: %s", name, path, err)
}
func (it *Manager) DownloadCRL() {
crlUrl := it.Config.Zerooo.Endpoint + "/crl"
download("CRL", it.GetCRLLocation(), crlUrl)
}
func (it *Manager) DownloadDH() {
dhUrl := it.Config.Zerooo.Endpoint + "/dh.pem"
download("DH", it.GetDHLocation(), dhUrl)
}
func (it *Manager) IsOpenVPNReady() bool {
hasConfig := fileExists(it.GetOpenVPNConfigLocation())
hasCert := fileExists(it.GetCertificateLocation())
return hasConfig && hasCert
}

@ -0,0 +1,73 @@
package service
import (
"log"
"os"
"os/exec"
)
type OpenVPN struct {
Config *ZeroooConfig
Manager *Manager
shouldStop bool
cmd *exec.Cmd
}
func NewOpenVPN(manager *Manager) *OpenVPN {
return &OpenVPN{
Config: &manager.Config.Zerooo,
Manager: manager,
}
}
func (it *OpenVPN) Stop() {
it.shouldStop = true
if it.cmd != nil && it.cmd.Process != nil {
it.cmd.Process.Kill()
}
}
func (it *OpenVPN) GetConfigLocation() string {
return it.Config.Location + "/server.conf"
}
func (it *OpenVPN) Restart() {
if it.cmd != nil && it.cmd.Process != nil {
it.cmd.Process.Kill()
return
}
it.Start()
}
func (it *OpenVPN) Start() {
if !it.Manager.IsOpenVPNReady() {
log.Printf("OpenVPN not ready yet.")
return
}
log.Printf("Starting OpenVPN")
it.cmd = exec.Command("openvpn", "--config", it.GetConfigLocation())
it.cmd.Start()
go func() {
it.cmd.Wait()
if it.shouldStop {
log.Printf("OpenVPN has stopped (exit code: %d)", it.cmd.ProcessState.ExitCode())
return
}
log.Printf("OpenVPN has stopped (exit code: %d), restarting", it.cmd.ProcessState.ExitCode())
it.Start()
}()
}
func (it *OpenVPN) UpdateConfig(s string) error {
f, err := os.OpenFile(it.GetConfigLocation(), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(s)
return err
}

@ -0,0 +1,17 @@
package service
import (
"log"
"os"
)
func Check(err error, msg string, v ...interface{}) {
if err != nil {
Error(msg, v...)
}
}
func Error(msg string, v ...interface{}) {
log.Fatalf(msg+"\n", v...)
os.Exit(1)
}

@ -0,0 +1,24 @@
package cmd
import (
"git.cijber.net/zer.ooo/service"
"github.com/spf13/cobra"
)
var daemonCmd = &cobra.Command{
Use: "daemon",
Short: "Starts the zer.ooo service daemon",
Long: "Will start running the zer.ooo service daemon, keeping OpenVPN up",
Run: func(cmd *cobra.Command, args []string) {
config, err := service.LoadConfig(ConfigPath)
service.Check(err, "Failed loading config, err: %s", err)
mgmt := service.NewManager(*config)
mgmt.Init()
mgmt.Daemon()
},
}
func init() {
}

@ -0,0 +1,49 @@
package cmd
import (
"git.cijber.net/zer.ooo/service"
"github.com/spf13/cobra"
"os"
"path/filepath"
)
var (
endpoint string
location string
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialises an zer.ooo server install",
Long: "Init (zerooo init) will initialise a server",
Run: func(cmd *cobra.Command, args []string) {
config, err := service.LoadConfig(ConfigPath)
service.Check(err, "Failed reading config, err: %s", err)
if (cmd.Flag("endpoint").Changed) {
config.Zerooo.Endpoint = endpoint
}
if (cmd.Flag("location").Changed) {
if location[0] != '/' {
cwd, err := os.Getwd()
service.Check(err, "Can't get current working directory, err: %s", err)
location = filepath.Join(cwd, location)
}
config.Zerooo.Location = location
}
service.CreateConfig(ConfigPath, config)
mgmt := service.NewManager(*config)
mgmt.Init()
mgmt.Register()
},
}
func init() {
f := initCmd.Flags()
f.StringVar(&endpoint, "endpoint", "https://zer.ooo", "Endpoint to register this server")
f.StringVar(&location, "location", "/etc/zerooo", "Location of OpenVPN installation")
}

@ -0,0 +1,54 @@
/*
Copyright © 2019 eater
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var ConfigPath string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "zerooo",
Short: "",
Long: `Zer.ooo service manager`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&ConfigPath, "config", "/etc/zerooo/service.toml", "config file")
rootCmd.AddCommand(
initCmd,
daemonCmd,
)
}

@ -0,0 +1,7 @@
package main
import "git.cijber.net/zer.ooo/service/zerooo/cmd"
func main() {
cmd.Execute()
}
Loading…
Cancel
Save