feat: Commands to generate server and client certs.

This commit is contained in:
jmug committed 2026-10-23 00:12:27 -07:00
1 parent abecfc3708
commit e8503ab141
3 files changed
+244 -2

No files matched your search

+5 -2
View File
@@ -8,9 +8,13 @@ import (
var Name string
var OutDir string
// Needed by all subcommands that generate certificates.
// Needed by subcommands that generate certificates.
var CommonName string
// Needed by subcommands that generate leaf certificates.
var CAFile string
var CAKeyFile string
func init() {
rootCmd.AddCommand(generateCmd)
@@ -31,7 +35,6 @@ func init() {
".",
"output directory for artifacts produced by this command, defaults to the current directory",
)
generateCmd.MarkPersistentFlagRequired("output-dir")
generateCmd.MarkPersistentFlagDirname("output-dir")
}
+104
View File
@@ -0,0 +1,104 @@
package hmpki
import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"path"
"time"
"git.handmadecities.com/HMC/shhh/pkg/pki"
"github.com/spf13/cobra"
)
func init() {
generateCmd.AddCommand(generateClientCertCmd)
generateClientCertCmd.Flags().StringVar(
&CAFile,
"ca",
"",
"File for the CA that will sign the leaf",
)
generateClientCertCmd.MarkFlagRequired("ca")
generateClientCertCmd.MarkFlagFilename("ca")
generateClientCertCmd.Flags().StringVar(
&CAKeyFile,
"ca-key",
"",
"File for the CA private key that will sign the leaf",
)
generateClientCertCmd.MarkFlagRequired("ca-key")
generateClientCertCmd.MarkFlagFilename("ca-key")
generateClientCertCmd.Flags().StringVar(
&CommonName,
"common-name",
"",
"Common Name for the CA",
)
generateClientCertCmd.MarkFlagRequired("common-name")
}
var generateClientCertCmd = &cobra.Command{
Use: "client-cert",
Short: "Generate a leaf client certificate and private key",
RunE: func(cmd *cobra.Command, args []string) error {
ca, err := pki.ParseCertFromFile(CAFile)
if err != nil {
return err
}
caKey, err := pki.ParseKeyFromFile(CAKeyFile)
if err != nil {
return err
}
certKey := pki.NewKey()
caTmpl := &x509.Certificate{
SerialNumber: pki.NewCertSerialNumber(),
Subject: pkix.Name{
CommonName: CommonName,
},
// Leaf certs valid for 1 year.
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
// This cert is a CA.
IsCA: false,
// Disambiguate `IsCA`
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
caBytes, err := x509.CreateCertificate(
rand.Reader,
caTmpl,
// CA is the parent.
ca,
// The cert embeds its own public key.
&certKey.PublicKey,
// The CA's private key is the one signing the leaf cert.
caKey,
)
caPEMBytes := pem.EncodeToMemory(&pem.Block{
Type: pki.PEMTypeCertificate,
Bytes: caBytes,
})
// Write cert to disk.
cafn := path.Join(OutDir, Name+".crt")
err = createFileWith(cafn, caPEMBytes, pki.CertFilePerms)
if err != nil {
return err
}
// Write cert private key to disk.
keyfn := path.Join(OutDir, Name+".key")
err = createFileWith(keyfn, pki.MarshalKeyToPEM(caKey), pki.PrivateKeyFilePerms)
if err != nil {
return err
}
return nil
},
}
+135
View File
@@ -0,0 +1,135 @@
package hmpki
import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net"
"path"
"time"
"git.handmadecities.com/HMC/shhh/pkg/pki"
"github.com/spf13/cobra"
)
var StringIPs []string
var DNSNames []string
func init() {
generateCmd.AddCommand(generateServerCertCmd)
generateServerCertCmd.Flags().StringVar(
&CAFile,
"ca",
"",
"File for the CA that will sign the leaf",
)
generateServerCertCmd.MarkFlagRequired("ca")
generateServerCertCmd.MarkFlagFilename("ca")
generateServerCertCmd.Flags().StringVar(
&CAKeyFile,
"ca-key",
"",
"File for the CA private key that will sign the leaf",
)
generateServerCertCmd.MarkFlagRequired("ca-key")
generateServerCertCmd.MarkFlagFilename("ca-key")
generateServerCertCmd.Flags().StringVar(
&CommonName,
"common-name",
"",
"Common Name for the CA",
)
generateServerCertCmd.MarkFlagRequired("common-name")
generateServerCertCmd.Flags().StringArrayVar(
&StringIPs,
"ips",
nil,
"IP Addresses that the cert will bind",
)
// TODO: Validate them to make sure they're valid.
generateServerCertCmd.Flags().StringArrayVar(
&DNSNames,
"dns-names",
nil,
"DNS names that the cert will bind",
)
// At least one IP or DNS name should be provided.
generateServerCertCmd.MarkFlagsOneRequired("ips", "dns-names")
}
var generateServerCertCmd = &cobra.Command{
Use: "server-cert",
Short: "Generate a leaf server certificate and private key",
RunE: func(cmd *cobra.Command, args []string) error {
ca, err := pki.ParseCertFromFile(CAFile)
if err != nil {
return err
}
caKey, err := pki.ParseKeyFromFile(CAKeyFile)
if err != nil {
return err
}
var ips = make([]net.IP, 0, len(StringIPs))
for _, sip := range StringIPs {
ip := net.ParseIP(sip)
if ip == nil {
return fmt.Errorf("ip %s is invalid", sip)
}
ips = append(ips, ip)
}
certKey := pki.NewKey()
caTmpl := &x509.Certificate{
SerialNumber: pki.NewCertSerialNumber(),
Subject: pkix.Name{
CommonName: CommonName,
},
DNSNames: DNSNames,
IPAddresses: ips,
// Leaf certs valid for 1 year.
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
// This cert is a CA.
IsCA: false,
// Disambiguate `IsCA`
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
caBytes, err := x509.CreateCertificate(
rand.Reader,
caTmpl,
// CA is the parent.
ca,
// The cert embeds its own public key.
&certKey.PublicKey,
// The CA's private key is the one signing the leaf cert.
caKey,
)
caPEMBytes := pem.EncodeToMemory(&pem.Block{
Type: pki.PEMTypeCertificate,
Bytes: caBytes,
})
// Write cert to disk.
cafn := path.Join(OutDir, Name+".crt")
err = createFileWith(cafn, caPEMBytes, pki.CertFilePerms)
if err != nil {
return err
}
// Write cert private key to disk.
keyfn := path.Join(OutDir, Name+".key")
err = createFileWith(keyfn, pki.MarshalKeyToPEM(caKey), pki.PrivateKeyFilePerms)
if err != nil {
return err
}
return nil
},
}