albertogviana/easyrsa

View on GitHub
easyrsa.go

Summary

Maintainability
A
40 mins
Test Coverage
package easyrsa

import (
    "bytes"
    "errors"
    "fmt"
    "io"
    "os"
    "os/exec"
    "path"
    "regexp"
)

// EasyRSA struct
type EasyRSA struct {
    Config
}

// Config has all the configuration needed to run easyrsa
type Config struct {
    BinDir           string // Easy-RSA top-level dir, where the easyrsa script is located.
    PKIDir           string // Used to hold all PKI-specific files
    CommonName       string // Common name used to generate the certificates
    KeySize          int    // Set the keysize in bits to generate
    CAExpire         int    // In how many days should the root CA key expire?
    ServerName       string // Server name
    CountryCode      string
    Province         string
    City             string
    Organization     string
    Email            string
    OrganizationUnit string
}

const errCAAlreadyExist = "Easy-RSA error:\n\nUnable to create a CA as you already seem to have one set up.\nIf you intended to start a new CA, run init-pki first.\n"

// NewEasyRSA returns an instance of EasyRSA
func NewEasyRSA(config Config) (*EasyRSA, error) {
    err := validate(config)
    if err != nil {
        return nil, err
    }

    if config.KeySize == 0 {
        config.KeySize = 2048
    }

    if config.CAExpire == 0 {
        config.CAExpire = 3650
    }

    easyRSA := &EasyRSA{
        config,
    }

    return easyRSA, nil
}

// InitPKI initializes a directory for the PKI.
func (e *EasyRSA) InitPKI() error {
    _, privateErr := os.Stat(path.Join(e.PKIDir, "private"))
    _, reqsErr := os.Stat(path.Join(e.PKIDir, "reqs"))

    if privateErr == nil && reqsErr == nil {
        return nil
    }

    return e.run("init-pki")
}

// BuildCA generates the Certificate Authority (CA)
func (e *EasyRSA) BuildCA() error {
    err := e.run("build-ca", "nopass")

    if err == nil {
        return nil
    }

    re := regexp.MustCompile("Easy-RSA error:(?s)(.*)")
    regexResult := re.FindString(string(err.Error()))

    if regexResult == errCAAlreadyExist {
        return errors.New(errCAAlreadyExist)
    }

    return err
}

// GenReq generates a keypair and request
func (e *EasyRSA) GenReq(requestName string) error {
    return e.run("gen-req", requestName, "nopass")
}

// SignReq signs a request, and you can have the following types:
//     - client - A TLS client, suitable for a VPN user or web browser (web client)
//     - server - A TLS server, suitable for a VPN or web server
func (e *EasyRSA) SignReq(typeSign, requestName string) error {
    if typeSign != "server" && typeSign != "client" {
        return errors.New("invalid type, please use server or client")
    }

    return e.run("sign-req", typeSign, requestName)
}

// ImportReq import requests from external systems that are requesting
// a signed certificate from this CA
func (e *EasyRSA) ImportReq(requestFile, requestName string) error {
    if _, err := os.Stat(requestFile); os.IsNotExist(err) {
        return err
    }
    return e.run("import-req", requestFile, requestName)
}

// GenDH creates a strong Diffie-Hellman key to use during key exchange
func (e *EasyRSA) GenDH() error {
    return e.run("gen-dh")
}

// Revoke revokes a certificate, after you revoke the certificate it is a
// good practive to update the CRL file, and send it to the VPN Server
// https://github.com/OpenVPN/easy-rsa/blob/v3.0.6/doc/EasyRSA-Readme.md#revoking-and-publishing-crls
func (e *EasyRSA) Revoke(requestName string) error {
    return e.run("revoke", requestName)
}

// GenCRL generates a CRL suitable for publishing to systems that rely, otherwise
// the revoke certificate will be available
func (e *EasyRSA) GenCRL() error {
    return e.run("gen-crl")
}

func (e *EasyRSA) getEnvironmentVariable() []string {
    var vars []string

    vars = append(vars, fmt.Sprintf("EASYRSA=%s", e.BinDir))
    vars = append(vars, fmt.Sprintf("EASYRSA_PKI=%s", e.PKIDir))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_CN=%s", e.CommonName))
    vars = append(vars, fmt.Sprintf("EASYRSA_CA_EXPIRE=%d", e.CAExpire))
    vars = append(vars, fmt.Sprintf("EASYRSA_KEY_SIZE=%d", e.KeySize))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_COUNTRY=%s", e.CountryCode))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_PROVINCE=%s", e.Province))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_CITY=%s", e.City))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_ORG=%s", e.Organization))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_EMAIL=%s", e.Email))
    vars = append(vars, fmt.Sprintf("EASYRSA_REQ_OU=%s", e.OrganizationUnit))
    vars = append(vars, "EASYRSA_BATCH=1")

    return vars
}

func (e *EasyRSA) run(args ...string) error {
    environment := e.getEnvironmentVariable()

    var stderrBuf bytes.Buffer

    cmd := exec.Command(path.Join(e.BinDir, "easyrsa"), args...)
    cmd.Env = append(os.Environ(), environment...)

    stderrIn, _ := cmd.StderrPipe()
    stderr := io.MultiWriter(os.Stderr, &stderrBuf)

    cmd.Stdout = os.Stdout

    err := cmd.Start()
    if err != nil {
        return fmt.Errorf("cmd.Start() failed with '%s'", err)
    }

    go func() {
        io.Copy(stderr, stderrIn)
    }()

    err = cmd.Wait()

    if err == nil {
        return nil
    }

    return errors.New(string(stderrBuf.Bytes()))
}

func validate(config Config) error {
    if config.BinDir == "" {
        return errors.New("the path to easy-rsa directory was not define")
    }

    if config.PKIDir == "" {
        return errors.New("the path to the pki directory was not define")
    }

    if config.CommonName == "" {
        return errors.New("the common name was not define")
    }

    if _, err := os.Stat(config.BinDir); os.IsNotExist(err) {
        return err
    }

    if _, err := os.Stat(path.Join(config.BinDir, "easyrsa")); os.IsNotExist(err) {
        return err
    }

    return nil
}