Major refactoring.

+ Many lib functions have been split out into separate packages.
+ Adding cert/key generation tooling.
+ Add new time.Duration parser.
This commit is contained in:
2025-11-19 01:34:26 -08:00
parent 90a48a1890
commit 154d5a6c2e
20 changed files with 459 additions and 36 deletions

211
certlib/certgen/config.go Normal file
View File

@@ -0,0 +1,211 @@
package certgen
import (
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"strings"
"time"
"git.wntrmute.dev/kyle/goutils/lib"
)
type KeySpec struct {
Algorithm string `yaml:"algorithm"`
Size int `yaml:"size"`
}
func (ks KeySpec) Generate() (crypto.PublicKey, crypto.PrivateKey, error) {
switch strings.ToLower(ks.Algorithm) {
case "rsa":
return GenerateKey(x509.RSA, ks.Size)
case "ecdsa":
return GenerateKey(x509.ECDSA, ks.Size)
case "ed25519":
return GenerateKey(x509.Ed25519, 0)
default:
return nil, nil, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
}
}
func (ks KeySpec) SigningAlgorithm() (x509.SignatureAlgorithm, error) {
switch strings.ToLower(ks.Algorithm) {
case "rsa":
return x509.SHA512WithRSAPSS, nil
case "ecdsa":
return x509.ECDSAWithSHA512, nil
case "ed25519":
return x509.PureEd25519, nil
default:
return 0, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
}
}
type Subject struct {
CommonName string `yaml:"common_name"`
Country string `yaml:"country"`
Locality string `yaml:"locality"`
Province string `yaml:"province"`
Organization string `yaml:"organization"`
OrganizationalUnit string `yaml:"organizational_unit"`
Email string `yaml:"email"`
DNSNames []string `yaml:"dns"`
IPAddresses []string `yaml:"ips"`
}
type CertificateRequest struct {
KeySpec KeySpec `yaml:"key"`
Subject Subject `yaml:"subject"`
Profile Profile `yaml:"profile"`
}
func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateRequest, error) {
pub, priv, err := cs.KeySpec.Generate()
if err != nil {
return nil, nil, err
}
subject := pkix.Name{}
subject.CommonName = cs.Subject.CommonName
subject.Country = []string{cs.Subject.Country}
subject.Locality = []string{cs.Subject.Locality}
subject.Province = []string{cs.Subject.Province}
subject.Organization = []string{cs.Subject.Organization}
subject.OrganizationalUnit = []string{cs.Subject.OrganizationalUnit}
ipAddresses := make([]net.IP, 0, len(cs.Subject.IPAddresses))
for i, ip := range cs.Subject.IPAddresses {
ipAddresses = append(ipAddresses, net.ParseIP(ip))
if ipAddresses[i] == nil {
return nil, nil, fmt.Errorf("invalid IP address: %s", ip)
}
}
req := &x509.CertificateRequest{
PublicKeyAlgorithm: 0,
PublicKey: pub,
Subject: subject,
DNSNames: cs.Subject.DNSNames,
IPAddresses: ipAddresses,
}
reqBytes, err := x509.CreateCertificateRequest(rand.Reader, req, priv)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate request: %w", err)
}
req, err = x509.ParseCertificateRequest(reqBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse certificate request: %w", err)
}
return priv, req, nil
}
type Profile struct {
IsCA bool `yaml:"is_ca"`
PathLen int `yaml:"path_len"`
KeyUse string `yaml:"key_uses"`
ExtKeyUsages []string `yaml:"ext_key_usages"`
Expiry string `yaml:"expiry"`
}
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
serial, err := SerialNumber()
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
expiry, err := lib.ParseDuration(p.Expiry)
if err != nil {
return nil, fmt.Errorf("parsing expiry: %w", err)
}
certTemplate := &x509.Certificate{
SignatureAlgorithm: req.SignatureAlgorithm,
PublicKeyAlgorithm: req.PublicKeyAlgorithm,
PublicKey: req.PublicKey,
SerialNumber: serial,
Subject: req.Subject,
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(expiry),
BasicConstraintsValid: true,
IsCA: p.IsCA,
MaxPathLen: p.PathLen,
DNSNames: req.DNSNames,
IPAddresses: req.IPAddresses,
}
var ok bool
certTemplate.KeyUsage, ok = keyUsageStrings[p.KeyUse]
if !ok {
return nil, fmt.Errorf("invalid key usage: %s", p.KeyUse)
}
var eku x509.ExtKeyUsage
for _, extKeyUsage := range p.ExtKeyUsages {
eku, ok = extKeyUsageStrings[extKeyUsage]
if !ok {
return nil, fmt.Errorf("invalid extended key usage: %s", extKeyUsage)
}
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
}
return certTemplate, nil
}
func (p Profile) SignRequest(parent *x509.Certificate, req *x509.CertificateRequest, priv crypto.PrivateKey) (*x509.Certificate, error) {
tpl, err := p.templateFromRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to create certificate template: %w", err)
}
certBytes, err := x509.CreateCertificate(rand.Reader, tpl, parent, req.PublicKey, priv)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
func (p Profile) SelfSign(req *x509.CertificateRequest, priv crypto.PrivateKey) (*x509.Certificate, error) {
certTemplate, err := p.templateFromRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to create certificate template: %w", err)
}
return p.SignRequest(certTemplate, req, priv)
}
func SerialNumber() (*big.Int, error) {
serialNumberBytes := make([]byte, 20)
_, err := rand.Read(serialNumberBytes)
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
return new(big.Int).SetBytes(serialNumberBytes), nil
}
// GenerateSelfSigned generates a self-signed certificate using the given certificate request.
func GenerateSelfSigned(creq *CertificateRequest) (*x509.Certificate, crypto.PrivateKey, error) {
priv, req, err := creq.Generate()
if err != nil {
return nil, nil, fmt.Errorf("failed to generate certificate request: %w", err)
}
cert, err := creq.Profile.SelfSign(req, priv)
if err != nil {
return nil, nil, fmt.Errorf("failed to self-sign certificate: %w", err)
}
return cert, priv, nil
}

View File

@@ -1,11 +1,60 @@
package certgen
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"errors"
"fmt"
)
var (
oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 110}
)
func GenerateKey() {}
func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicKey, crypto.PrivateKey, error) {
var key crypto.PrivateKey
var pub crypto.PublicKey
var err error
switch algorithm {
case x509.RSA:
pub, key, err = ed25519.GenerateKey(rand.Reader)
case x509.Ed25519:
key, err = rsa.GenerateKey(rand.Reader, bitSize)
if err == nil {
pub = key.(*rsa.PrivateKey).Public()
}
case x509.ECDSA:
var curve elliptic.Curve
switch bitSize {
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
default:
return nil, nil, fmt.Errorf("unsupported curve size %d", bitSize)
}
key, err = ecdsa.GenerateKey(curve, rand.Reader)
if err == nil {
pub = key.(*ecdsa.PrivateKey).Public()
}
default:
err = errors.New("unsupported algorithm")
}
if err != nil {
return nil, nil, err
}
return pub, key, nil
}

32
certlib/certgen/ku.go Normal file
View File

@@ -0,0 +1,32 @@
package certgen
import "crypto/x509"
var keyUsageStrings = map[string]x509.KeyUsage{
"signing": x509.KeyUsageDigitalSignature,
"digital signature": x509.KeyUsageDigitalSignature,
"content commitment": x509.KeyUsageContentCommitment,
"key encipherment": x509.KeyUsageKeyEncipherment,
"key agreement": x509.KeyUsageKeyAgreement,
"data encipherment": x509.KeyUsageDataEncipherment,
"cert sign": x509.KeyUsageCertSign,
"crl sign": x509.KeyUsageCRLSign,
"encipher only": x509.KeyUsageEncipherOnly,
"decipher only": x509.KeyUsageDecipherOnly,
}
var extKeyUsageStrings = map[string]x509.ExtKeyUsage{
"any": x509.ExtKeyUsageAny,
"server auth": x509.ExtKeyUsageServerAuth,
"client auth": x509.ExtKeyUsageClientAuth,
"code signing": x509.ExtKeyUsageCodeSigning,
"email protection": x509.ExtKeyUsageEmailProtection,
"s/mime": x509.ExtKeyUsageEmailProtection,
"ipsec end system": x509.ExtKeyUsageIPSECEndSystem,
"ipsec tunnel": x509.ExtKeyUsageIPSECTunnel,
"ipsec user": x509.ExtKeyUsageIPSECUser,
"timestamping": x509.ExtKeyUsageTimeStamping,
"ocsp signing": x509.ExtKeyUsageOCSPSigning,
"microsoft sgc": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
"netscape sgc": x509.ExtKeyUsageNetscapeServerGatedCrypto,
}