Documentation menu

Go package

The CLI is built on github.com/bcollard/homepki/pkg/pki. Import it to issue and verify certificates in-process, for example in a test harness or another CLI, without writing files or calling the binary.

Install

$ go get github.com/bcollard/homepki@v0.8.0

The package has no cobra dependency. It uses the standard library for keys, signing and verification, and software.sslmate.com/src/go-pkcs12 for PKCS#12 output. The API follows the CLI's releases; there is no separate compatibility promise before v1.

Issue a hierarchy

This program creates a root CA limited to .klimax.internal, an intermediate under it and a 24-hour server leaf, verifies the chain, and writes the key, the leaf and a CA bundle.

package main

import (
	"crypto/x509"
	"crypto/x509/pkix"
	"log"
	"time"

	"github.com/bcollard/homepki/pkg/pki"
)

func main() {
	// Root CA, limited to names under .klimax.internal.
	rootKey, err := pki.GenerateKey("ecdsa")
	if err != nil {
		log.Fatal(err)
	}
	nc := pki.NameConstraints{}.PermitDNS(".klimax.internal")
	root, err := pki.SelfSignRoot(rootKey,
		pkix.Name{Organization: []string{"klimax-internal"}, CommonName: "klimax.internal"},
		nc, 0) // 0 = the default, 2190 days
	if err != nil {
		log.Fatal(err)
	}

	// Intermediate CA signed by the root.
	interKey, err := pki.GenerateKey("ecdsa")
	if err != nil {
		log.Fatal(err)
	}
	inter, err := pki.SignIntermediate(interKey.Public(),
		pkix.Name{Organization: []string{"klimax-internal"}, OrganizationalUnit: []string{"bu1"}, CommonName: "bu1.klimax.internal"},
		pki.NameConstraints{}, 0, root, rootKey)
	if err != nil {
		log.Fatal(err)
	}

	// Server leaf valid for 24 hours.
	leafKey, err := pki.GenerateKey("ecdsa")
	if err != nil {
		log.Fatal(err)
	}
	sans, err := pki.ParseSANs([]string{"gw.bu1.klimax.internal", "10.0.0.5"})
	if err != nil {
		log.Fatal(err)
	}
	leaf, err := pki.SignLeaf(leafKey.Public(),
		pkix.Name{CommonName: "gw.bu1.klimax.internal"},
		sans, pki.ServerLeaf, 24*time.Hour, inter, interKey)
	if err != nil {
		log.Fatal(err)
	}

	// Verify before writing: this is where name constraints are enforced.
	if err := pki.VerifyChain(leaf, inter, root, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}); err != nil {
		log.Fatal(err)
	}

	if err := pki.WriteKey("gw.key", leafKey); err != nil {
		log.Fatal(err)
	}
	if err := pki.WriteCerts("gw.crt", leaf); err != nil {
		log.Fatal(err)
	}
	if err := pki.WriteCerts("ca-chain.crt", inter, root); err != nil {
		log.Fatal(err)
	}
}
  • Key types are the --key-type values: rsa (2048-bit), ecdsa/ecdsa-p256, ecdsa-p384, ecdsa-p521, ed25519. KeyTypes() lists them and ValidateKeyType checks one.
  • Validity is a time.Duration; 0 selects the default (CAValidityDays = 2190, LeafValidityDays = 365). A certificate is capped at its issuer's NotAfter. ParseValidity("90d", def) reads the CLI's format and Days(n) converts days.
  • SANs: ParseSANs takes the --san syntax. Bare values become IP or DNS, and DNS:, IP:, email:, URI: force a type. You can also fill pki.SANs{DNS, IPs, Emails, URIs} directly.
  • Leaf kind: pki.ServerLeaf or pki.ClientLeaf sets the extended key usage. RSA server leaves also get keyEncipherment.
  • Serials are random 128-bit values, NotBefore is 5 minutes in the past, and the signature algorithm follows the issuer's key.
ℹ

The package does not impose the CLI's naming. The CLI derives O, OU and CN from the domain and names, but SignLeaf signs whatever pkix.Name you pass. ValidateCSRSubject is the policy check the sign command applies to requests.

Name constraints

Build NameConstraints in code with the Permit* and Exclude* methods. Each returns a copy, so calls chain and never modify the value they start from.

lan, err := pki.ParseIPRange("10.0.0.0/8") // CIDR or address/netmask
if err != nil {
	log.Fatal(err)
}
nc := pki.NameConstraints{}.
	PermitDNS(".klimax.internal").
	ExcludeDNS("admin.klimax.internal").
	PermitIP(lan)

The methods are PermitDNS, ExcludeDNS, PermitIP, ExcludeIP, PermitEmail, ExcludeEmail, PermitURI and ExcludeURI. ParseNameConstraints accepts the openssl syntax of --name-constraint instead and produces the same value. The extension is written as critical. Constraints are checked when you verify, not when you sign: use IsNameConstraintViolation(err) to recognise that failure from VerifyChain.

Verification

FunctionChecks
VerifyRoot(root)A CA certificate, signed by its own key, valid now.
VerifyIntermediate(inter, root)A CA certificate signed by root, valid now. The root's name constraints apply to the intermediate's names.
VerifyChain(leaf, inter, root, ekus)The leaf chains to the root through the intermediate, with the given extended key usages and every name constraint.

All three return an error for a nil certificate instead of panicking. VerifyRootCert, VerifyIntermediateCert and VerifyLeafCert do the same checks from file paths. CheckKeyPair(cert, key) reports a key that does not belong to a certificate.

Reading and writing files

  • LoadCert(path) returns the first certificate in a PEM file. LoadCerts(path) returns all of them in file order, for a chain file such as bu1-intermediate-ca-chain.crt (intermediate, then root). Both skip text and PEM blocks of other types.
  • LoadKey(path) reads PKCS#8, PKCS#1 (RSA) or SEC 1 (EC) PEM keys.
  • WriteKey(path, key) writes an unencrypted PKCS#8 PEM file with mode 0600. WriteCerts(path, certs...) writes one or more certificates in order with mode 0644.
  • LoadCSR, SANsFromCSR and ValidateCSRSubject cover requests made elsewhere.
chain, err := pki.LoadCerts("bu1-intermediate-ca-chain.crt")
if err != nil {
	log.Fatal(err)
}
err = pki.VerifyIntermediate(chain[0], chain[1])

Revocation lists

SignCRL issues a new CRL holding the entries of the previous one (or nil) plus new ones, with the next CRL number. nextUpdate is the validity from now, capped at the issuer's expiry.

prev, err := pki.LoadCRL("bu1-intermediate-ca.crl", inter) // nil, nil when the file does not exist
if err != nil {
	log.Fatal(err)
}
entry := x509.RevocationListEntry{SerialNumber: leaf.SerialNumber, RevocationTime: time.Now(), ReasonCode: 1}
crl, err := pki.SignCRL(prev, []x509.RevocationListEntry{entry}, 0, inter, interKey)
if err != nil {
	log.Fatal(err)
}
err = pki.WriteCRLs("bu1-intermediate-ca.crl", crl)

LoadCRL also checks that the issuer signed the file. FindRevoked(crl, serial) returns the matching entry or nil. ParseRevocationReason, RevocationReasonName and RevocationReasons map RFC 5280 reason names to codes.

PKCS#12

err := pki.WritePKCS12("gw.p12", leafKey, leaf, []*x509.Certificate{inter, root}, pki.DefaultPKCS12Password)

The file holds the key, the leaf and the chain, with mode 0600. It uses the 3DES/SHA-1 encoding that Java, macOS Keychain, Windows and OpenSSL 3 read without extra options. DefaultPKCS12Password is changeit.

Trust store helpers

The functions behind homepki trust build commands without running them, so you can run them yourself or inspect them in tests:

  • macOS: MacOSTrustInstallArgs, MacOSTrustUninstallArgs, MacOSTrustStatusArgs (the security command).
  • Linux: DetectLinuxStore() returns a LinuxStore whose AnchorPath, InstallCommands and UninstallCommands cover Debian, Fedora, Arch and openSUSE layouts. SystemTrusts(cert) checks the platform store; on Linux it rereads the bundle files on each call.
  • NSS: NSSDatabases(home), FindCertutil(), NSSNickname, NSSInstallArgs, NSSUninstallArgs, NSSStatusArgs.
  • Java: JavaTrustStore(javaHome), JavaAlias, JavaInstallArgs, JavaUninstallArgs, JavaStatusArgs, JavaStorePassword.
  • Sudo(argv) prefixes sudo unless the process runs as root. RunInteractive and RunQuiet execute a command.

API summary

AreaFunctions and types
KeysGenerateKey, ValidateKeyType, KeyTypes, WriteKey, LoadKey, CheckKeyPair
IssuanceSelfSignRoot, SignIntermediate, SignLeaf, LeafKind (ServerLeaf, ClientLeaf), SANs, ParseSANs, ParseValidity, Days, CAValidityDays, LeafValidityDays
Name constraintsNameConstraints, Permit*/Exclude*, ParseNameConstraints, ParseIPRange, IsNameConstraintViolation
VerificationVerifyRoot, VerifyIntermediate, VerifyChain, VerifyRootCert, VerifyIntermediateCert, VerifyLeafCert, GetCertExpiry
FilesLoadCert, LoadCerts, WriteCerts, CreateDirectory, CreatePrivateDirectory, GetRootCALiteralName
RequestsLoadCSR, SANsFromCSR, ValidateCSRSubject, LeafNameFromCN
RevocationSignCRL, LoadCRL, WriteCRLs, FindRevoked, ParseRevocationReason, RevocationReasonName, RevocationReasons, CRLValidityDays
PKCS#12WritePKCS12, DefaultPKCS12Password
Trust storesSystemTrusts, DetectLinuxStore, LinuxStore, NSS*, FindCertutil, Java*, MacOSTrust*Args, Sudo, IsWritable

Full signatures: go doc -all github.com/bcollard/homepki/pkg/pki, or pkg.go.dev.