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.
On this page
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-typevalues:rsa(2048-bit),ecdsa/ecdsa-p256,ecdsa-p384,ecdsa-p521,ed25519.KeyTypes()lists them andValidateKeyTypechecks one. - Validity is a
time.Duration;0selects the default (CAValidityDays= 2190,LeafValidityDays= 365). A certificate is capped at its issuer'sNotAfter.ParseValidity("90d", def)reads the CLI's format andDays(n)converts days. - SANs:
ParseSANstakes the--sansyntax. Bare values become IP or DNS, andDNS:,IP:,email:,URI:force a type. You can also fillpki.SANs{DNS, IPs, Emails, URIs}directly. - Leaf kind:
pki.ServerLeaforpki.ClientLeafsets the extended key usage. RSA server leaves also getkeyEncipherment. - Serials are random 128-bit values,
NotBeforeis 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
| Function | Checks |
|---|---|
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 asbu1-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 mode0600.WriteCerts(path, certs...)writes one or more certificates in order with mode0644.LoadCSR,SANsFromCSRandValidateCSRSubjectcover 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(thesecuritycommand). - Linux:
DetectLinuxStore()returns aLinuxStorewhoseAnchorPath,InstallCommandsandUninstallCommandscover 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)prefixessudounless the process runs as root.RunInteractiveandRunQuietexecute a command.
API summary
| Area | Functions and types |
|---|---|
| Keys | GenerateKey, ValidateKeyType, KeyTypes, WriteKey, LoadKey, CheckKeyPair |
| Issuance | SelfSignRoot, SignIntermediate, SignLeaf, LeafKind (ServerLeaf, ClientLeaf), SANs, ParseSANs, ParseValidity, Days, CAValidityDays, LeafValidityDays |
| Name constraints | NameConstraints, Permit*/Exclude*, ParseNameConstraints, ParseIPRange, IsNameConstraintViolation |
| Verification | VerifyRoot, VerifyIntermediate, VerifyChain, VerifyRootCert, VerifyIntermediateCert, VerifyLeafCert, GetCertExpiry |
| Files | LoadCert, LoadCerts, WriteCerts, CreateDirectory, CreatePrivateDirectory, GetRootCALiteralName |
| Requests | LoadCSR, SANsFromCSR, ValidateCSRSubject, LeafNameFromCN |
| Revocation | SignCRL, LoadCRL, WriteCRLs, FindRevoked, ParseRevocationReason, RevocationReasonName, RevocationReasons, CRLValidityDays |
| PKCS#12 | WritePKCS12, DefaultPKCS12Password |
| Trust stores | SystemTrusts, DetectLinuxStore, LinuxStore, NSS*, FindCertutil, Java*, MacOSTrust*Args, Sudo, IsWritable |
Full signatures: go doc -all github.com/bcollard/homepki/pkg/pki, or pkg.go.dev.