Part 2 of a small series into building a Public Key Infrastructure chain with Golang

Ok, yeah yeah yeah, Part 1 was a lot of primer (it’s really not, trust me) - where’s the action?!

Alrighty there ya Giddy Gopher, let’s start doing some Golang Programming! Let’s start down the path of creating custom Certificate Authorities and associated PKI!

Series Table of Contents

Directory Structure

It’s good to standardize on a common directory structure for your Certificate Authorities, be them Root, Intermediate, Intermediate-Intermediate, so on and so forth. This would normally be defined in something like an OpenSSL configuration file like so:

#
# OpenSSL configuration for the Root Certification Authority.
#

#
# This definition doesn't work if HOME isn't defined.
CA_HOME                 = .

#
# Default Certification Authority
[ ca ]
default_ca              = root_ca

#
# Root Certification Authority
[ root_ca ]
dir                     = $ENV::CA_HOME
certs                   = $dir/certs
serial                  = $dir/ca.serial
database                = $dir/ca.index
new_certs_dir           = $dir/newcerts
certificate             = $dir/ca.cert
private_key             = $dir/private/ca.key.pem
default_days            = 1826 # 5 years
crl                     = $dir/ca.crl
crl_dir                 = $dir/crl
crlnumber               = $dir/ca.crlnum

We can represent this in Golang with the following typed structure - this type defines what all the different folders and files are associated with any Certificate Authority:

// CertificateAuthorityPaths returns all the default paths generated by a new CA
type CertificateAuthorityPaths struct {
	RootCAPath               string
	RootCACertRequestsPath   string
	RootCACertsPath          string
	RootCACertRevListPath    string
	RootCANewCertsPath       string
	RootCACertKeysPath       string
	RootCAIntermediateCAPath string
	RootCACertIndexFilePath  string
	RootCACertSerialFilePath string
	RootCACrlnumFilePath     string
}

Now let’s create a function that will create the needed directories and return them to be used in other parts of our application.

// setupCAFileStructure creates the basic directories and files required by a new CA
func setupCAFileStructure(basePath string) CertificateAuthorityPaths {
	//Create root CA directory
	rootCAPath := basePath
	CreateDirectory(rootCAPath)

	// Create certificate requests (CSR) path
	rootCACertRequestsPath := rootCAPath + "/certreqs"
	CreateDirectory(rootCACertRequestsPath)

	// Create certs path
	rootCACertsPath := rootCAPath + "/certs"
	CreateDirectory(rootCACertsPath)

	// Create crls path
	rootCACertRevListPath := rootCAPath + "/crl"
	CreateDirectory(rootCACertRevListPath)

	// Create newcerts path (wtf is newcerts for vs certs?!)
	rootCANewCertsPath := rootCAPath + "/newcerts"
	CreateDirectory(rootCANewCertsPath)

	// Create private path for CA keys
	rootCACertKeysPath := rootCAPath + "/private"
	CreateDirectory(rootCACertKeysPath)

	// Create intermediate CA path
	rootCAIntermediateCAPath := rootCAPath + "/intermed-ca"
	CreateDirectory(rootCAIntermediateCAPath)

	//  CREATE INDEX DATABASE FILE
	rootCACertIndexFilePath := rootCAPath + "/ca.index"
	IndexFile, err := WriteFile(rootCACertIndexFilePath, "", 0600, false)
	check(err)
	if IndexFile {
		logStdOut("Created Index file")
	} else {
		logStdOut("Index file exists")
	}

	//  CREATE SERIAL FILE
	rootCACertSerialFilePath := rootCAPath + "/ca.serial"
	serialFile, err := WriteFile(rootCACertSerialFilePath, "01", 0600, false)
	check(err)
	if serialFile {
		logStdOut("Created serial file")
	} else {
		logStdOut("Serial file exists")
	}

	//  CREATE CERTIFICATE REVOCATION NUMBER FILE
	rootCACrlnumFilePath := rootCAPath + "/ca.crlnum"
	crlNumFile, err := WriteFile(rootCACrlnumFilePath, "00", 0600, false)
	check(err)
	if crlNumFile {
		logStdOut("Created crlnum file")
	} else {
		logStdOut("crlnum file exists")
	}

	return CertificateAuthorityPaths{
		RootCAPath:               rootCAPath,
		RootCACertRequestsPath:   rootCACertRequestsPath,
		RootCACertsPath:          rootCACertsPath,
		RootCACertRevListPath:    rootCACertRevListPath,
		RootCANewCertsPath:       rootCANewCertsPath,
		RootCACertKeysPath:       rootCACertKeysPath,
		RootCAIntermediateCAPath: rootCAIntermediateCAPath,
		RootCACertIndexFilePath:  rootCACertIndexFilePath,
		RootCACertSerialFilePath: rootCACertSerialFilePath,
		RootCACrlnumFilePath:     rootCACrlnumFilePath,
	}
}

The supporting functions required by that one are the following:

import (
	"github.com/gosimple/slug"
)

// slugger slugs a string
func slugger(textToSlug string) string {
	return slug.Make(textToSlug)
}

// check does error checking
func check(e error) {
	if e != nil {
		log.Printf("error: %v", e)
	}
}

// logStdOut just logs something to stdout
func logStdOut(s string) {
	log.Printf("%s\n", string(s))
}

// CreateDirectory is self explanitory
func CreateDirectory(path string) {
	log.Printf("Creating directory %s\n", path)
	_, err := os.Stat(path)
	if os.IsNotExist(err) {
		errDir := os.MkdirAll(path, 0755)
		check(errDir)
	}
}

// WriteFile creates a file only if it's new and populates it
func WriteFile(path string, content string, mode int, overwrite bool) (bool, error) {
	fileMode := os.FileMode(0600)
	if mode == 0 {
		fileMode = os.FileMode(0600)
	} else {
		fileMode = os.FileMode(mode)
	}
	fileCheck, err := FileExists(path)
	check(err)
	// If not, create one with a starting digit
	if !fileCheck {
		d1 := []byte(content)
		err = ioutil.WriteFile(path, d1, fileMode)
		check(err)
		return true, err
	}
	// If the file exists and we want to overwrite it
	if fileCheck && overwrite {
		d1 := []byte(content)
		err = ioutil.WriteFile(path, d1, fileMode)
		check(err)
		return true, err
	}
	return false, nil
}

Running the Directory Creation Function

Now piecing that all together is pretty easy.

  1. To create the directory structure of a Certificate Authority you need a base path - this could be a rootCAs/ directory, or rootCAs/my-cert-auth/intermed-ca/ as an Intermediate CA under My Cert Auth.
  2. Try to reference this base path as an absolute path.
  3. ?????
  4. PROFIT!!!1

Create a function to create a new CA and get this whole ball rolling by making the directory structure we need:

// CreateNewCA creates a new root Certificate Authority
func CreateNewCA(certificateID string) (bool, []string, error) {
    // Get the absolute path to what is the intended directory for the CA
    basePath, err := filepath.Abs("./pki-root/" + certificateID)
    check(err)
    
    // Create the needed file structure for the CA
    caPaths := setupCAFileStructure(basePath)
    if caPaths.RootCAPath != basePath {
        return false, []string{"Error creating CA file structure!"}, err
    }
    
    // More stuff to be added here later...
    
    return true, []string{"CA Created!"}, nil
}

Finally, call the CreateNewCA function with a certificateID - this would be a DNS/Filesystem compliant name, so a slug of the Common Name works well:

caCommonName := "Example Labs Root Certificate Authority"
commonNameSlug := slugger(caCommonName)

caCreated, messages, err := CreateNewCA(commonNameSlug)

if !caCreated {
    for _, msg := range messages {
        logStdOut(msg)
    }
    check(err)
}

We’ll be structuring our Certificate information like the Common Name a bit better down the road, but this will do for now as a proof-of-concept to get the file structure created for new CAs.

With this same setupCAFileStructure function we can create the file structure for Intermediate CAs as well just by switching the targetted base path.

Next Steps

Now that we have the directory structure functions set up, we need to start by creating Key Pairs - more on that in the next part of this series.